Forge测试预估Gas与主网真实交易差异原因及精准估算方案
我在使用forge test时发现,预估的Gas消耗量远低于以太坊主网真实交易的Gas消耗。以某笔主网交易为例,原交易消耗122532 Gas,但forge测试预估仅为106309,差距明显。
我尝试通过以下步骤复刻该交易:
- 从该交易所在区块的前一个区块进行fork
- 使用
vm.prank从同一发起者地址0xf5213a6a2f0890321712520b8048D9886c1A9900发送交易 - 调用目标地址
0xE592427A0AEce92De3Edee1F18E0157C05861564(UniswapV3 Router)并传入相同交易数据
测试代码如下:
contract EstimateGas is Test { function setUp() public { vm.createSelectFork("https://eth.drpc.org", 19400684); } function testEstimateGas() public { vm.prank(0xf5213a6a2f0890321712520b8048D9886c1A9900); bytes memory data = hex"414bf389000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000d1d2eb1b1e90b638588728b4130137d262c87cae0000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000f5213a6a2f0890321712520b8048d9886c1a990000000000000000000000000000000000000000000000000000000000065ecea7e00000000000000000000000000000000000000000000000058c3de02f05a8000000000000000000000000000000000000000000000000000000027c593b519ff0000000000000000000000000000000000000000000000000000000000000000"; address router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; (bool success,) = router.call(data); require(success, "fail"); } }
我使用的forge版本为forge 0.2.0 (42da942 2024-03-17T00:17:57.841914000Z)。
请问造成该差异的原因是什么?如何在forge测试中精准估算Gas?
一、差异产生的核心原因
1. 状态访问的Gas计算差异
主网交易中,合约访问冷状态(即该状态数据近期未被其他交易访问过)时,会收取更高的Gas费用(冷存储访问成本为2100 Gas,热存储仅为100 Gas)。而在fork测试环境中,所有状态数据默认都是热状态——因为fork操作会一次性加载相关状态到本地,导致forge计算时使用了热存储的Gas费率,从而低估实际消耗。
2. 交易固有开销未完全模拟
以太坊主网交易的基础Gas开销包含:
- 21000 Gas的基础交易费
- 每字节交易数据的Gas费用(非零字节4 Gas,零字节1 Gas)
forge测试中直接调用合约的方式,不会完全模拟主网交易的这些固有开销,尤其是交易数据的字节成本,会导致预估结果偏低。
3. EVM版本与优化差异
主网节点使用的EVM版本可能与forge测试环境的EVM存在细微差异,部分操作码的Gas成本在不同EVM版本中略有调整;另外,forge默认会启用一些编译优化选项,而主网合约的执行环境没有这些优化,也会导致Gas计算偏差。
4. 链上环境的细微差异
即使fork了前一个区块,主网交易执行时的区块上下文(比如区块哈希、时间戳等)与测试环境可能存在细微差别,部分合约逻辑会依赖这些上下文变量,进而影响实际Gas消耗。
二、精准估算Gas的解决方案
1. 模拟冷状态访问
在测试中手动标记状态为冷状态,使用vm.accesses(address account, bytes32[] storageSlots)方法,指定需要设置为冷访问的存储槽,让forge按照冷存储的Gas费率计算:
// 在调用合约前,标记相关存储槽为冷状态 vm.accesses(router, getRelevantStorageSlots());
(注:需要自行获取合约中被访问的存储槽,可通过forge inspect工具分析合约存储布局)
2. 完整模拟主网交易流程
不要直接调用合约,而是使用vm.sendTransaction方法模拟完整的主网交易,该方法会包含基础交易费和数据字节成本:
function testEstimateGas() public { vm.sendTransaction({ from: 0xf5213a6a2f0890321712520b8048D9886c1A9900, to: router, data: data, gasPrice: tx.gasPrice }); }
通过这种方式,forge会计算完整的交易Gas开销,结果更贴近主网真实值。
3. 关闭编译优化
在foundry.toml中关闭编译优化,确保测试环境与主网合约执行环境一致:
optimizer = false optimizer_runs = 0
4. 匹配主网EVM版本
在foundry.toml中指定与主网一致的EVM版本,比如以太坊上海升级后的版本:
evm_version = "shanghai"
5. 集成RPC节点的Gas估算(辅助验证)
可以通过调用以太坊节点的eth_estimateGas RPC方法,传入完整的交易参数,获取更精准的预估结果,在测试中可以集成该逻辑:
function testEstimateGasViaRPC() public { uint256 estimatedGas = vm.rpcUint( "eth_estimateGas", abi.encode( address(0xf5213a6a2f0890321712520b8048D9886c1A9900), router, data ) ); console.log("RPC Estimated Gas:", estimatedGas); }
内容的提问来源于stack exchange,提问作者Quantoor

