进阶 getfoundry.sh 2026-10-10 09:32:10 · 6 阅读

第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 上建议修改

评论 (0)