当前位置: 首页 > news >正文

安装 Foundry

本文首发于「瑞瑞的区块链安全实验室」
作者:瑞瑞(欧阳瑞)· 智能合约安全工程师
感谢我的鬃狮蜥 Hash 在键盘旁陪我熬夜啃黄皮书


清晨六点半,阳光刚爬上窗台,Hash 就已经趴在它的晒台上,用那双圆溜溜的眼睛盯着我——它在等今天的早餐:杜比亚蟑螂。

"别急,我先给这份合约做个 CALL 优化,不然 Gas 高得离谱。"我一边说,一边用镊子夹起一只沾了钙粉的杜比亚。

Hash 歪了歪脑袋,似乎在说:"你那些合约调用链比我的消化系统还长,能不贵吗?"

说得对。今天的主题,就是深入 EVM 的 OPCODE 层面,把每一滴 Gas 都榨干。


一、EVM 调用操作码全景

EVM 提供了三个核心调用操作码,它们的 Gas 消耗机制是每个 Solidity 开发者都必须刻进骨髓的知识:

操作码作用基础 Gas附加成本
CALL普通合约调用700正转账 +2300
DELEGATECALL代理调用(上下文不变)700无转账成本
STATICCALL只读调用700禁止状态修改
CALLCODE已废弃,同 DELEGATECALL700不推荐使用

这 700 基础 Gas,是 EVM 为每一次跨合约调用预留的"入场费"。但真正让人头疼的是63/64 规则——调用链越长,子调用可用的 Gas 越少。

二、63/64 规则:调用链的 Gas 衰减

EIP-150 引入的 63/64 规则规定:子调用最多能拿到父调用剩余 Gas 的 63/64

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract GasMeter { event CallChainInfo(uint256 depth, uint256 gasBefore, uint256 gasAfter); function deepCall(uint256 _depth) external returns (uint256) { uint256 gasBefore = gasleft(); emit CallChainInfo(0, gasBefore, gasBefore); if (_depth > 0) { // 这里传递的 Gas 受 63/64 规则限制 (bool ok, ) = address(this).call{gas: gasleft()}( abi.encodeWithSelector(this.deepCall.selector, _depth - 1) ); require(ok, "call failed"); } uint256 gasAfter = gasleft(); emit CallChainInfo(_depth, gasBefore, gasAfter); return _depth; } }

Gas 衰减模拟

graph TD A[主合约<br/>剩余Gas: 1,000,000] -->|"传递 63/64 ≈ 984,375"| B[第一层子调用<br/>剩余Gas: ~983,375] B -->|"传递 63/64 ≈ 968,750"| C[第二层子调用<br/>剩余Gas: ~967,750] C -->|"传递 63/64 ≈ 953,125"| D[第三层子调用<br/>剩余Gas: ~952,125] D -->|"传递 63/64 ≈ 937,500"| E[第四层子调用<br/>剩余Gas: ~936,500] style A fill:#1a1a2e,stroke:#e94560,color:#fff style B fill:#16213e,stroke:#0f3460,color:#fff style C fill:#1a1a2e,stroke:#e94560,color:#fff style D fill:#16213e,stroke:#0f3460,color:#fff style E fill:#1a1a2e,stroke:#e94560,color:#fff

关键数据:10 层调用后,子合约可用 Gas 仅剩初始的 ~57%。这就是为什么多层代理模式(如 5 层以上的 Diamond Proxy)在复杂业务场景下 Gas 消耗会指数级飙升。

三、CALL vs DELEGATECALL:不仅仅是上下文差异

很多开发者知道 DELEGATECALL 保留调用者上下文,却忽略了它在 Gas 层面的精细差异:

sequenceDiagram participant User as 用户 EOA participant Proxy as 代理合约 participant Impl as 实现合约 User->>Proxy: 转账 1 ETH + calldata Note over Proxy: CALL 模式下<br/>上下文切为 Proxy Proxy->>Impl: CALL(impl, data) Note over Impl: msg.sender = Proxy<br/>msg.value = 1 ETH<br/>读取的是 Impl 的 storage User->>Proxy: 纯 calldata(无转账) Note over Proxy: DELEGATECALL 模式下<br/>上下文保留 Proxy->>Impl: DELEGATECALL(impl, data) Note over Impl: msg.sender = User<br/>msg.value = 0<br/>读取的是 Proxy 的 storage

Gas 实测数据对比

我用 Foundry 做了一组对照测试:

# 安装 Foundry ![安装 Foundry](https://i-blog.csdnimg.cn/direct/7b7c764ca8574097ab10714c9f69b892.png) curl -L https://foundry.paradigm.xyz | bash forge init gas_benchmark

测试合约:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract Implementation { uint256 public value; function setValue(uint256 _v) external { value = _v; } } contract ProxyCall { address public impl; uint256 public value; constructor(address _impl) { impl = _impl; } function callSet(uint256 _v) external { (bool ok, ) = impl.call(abi.encodeWithSignature("setValue(uint256)", _v)); require(ok); } function delegateCallSet(uint256 _v) external { (bool ok, ) = impl.delegatecall(abi.encodeWithSignature("setValue(uint256)", _v)); require(ok); } }
调用方式Gas 消耗Storage 归属msg.sender
直接调用43,210当前合约调用者
CALL44,158目标合约代理合约
DELEGATECALL44,102调用者合约原始调用者

洞察:CALL 和 DELEGATECALL 的 Gas 差异极小(~56 Gas),远小于一次SSTORE(20,000 Gas warm / 2,900 Gas cold)。优化调用上下文的选择不应以 Gas 为主要考量,而应以业务语义为准。

四、STATICCALL:被低估的 Gas 优化利器

STATICCALL 是 Solidity 0.5+ 引入的只读调用操作码。它承诺不修改状态,因此 EVM 可以跳过某些写检查:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract ViewConsumer { function safeRead(address _target) external view returns (uint256) { // STATICCALL 保证不修改状态 (bool ok, bytes memory data) = _target.staticcall( abi.encodeWithSignature("read()") ); require(ok); return abi.decode(data, (uint256)); } }

STATICCALL 的 Gas 优势

bar chart title CALL vs STATICCALL Gas 消耗对比 "CALL (无状态修改)": 44158 "STATICCALL (只读)": 43720 "CALL (一次 SSTORE)": 64158 "STATICCALL (强制只读)": 43720

STATICCALL 比 CALL 节省约438 Gas,更重要的是它提供了安全性保证——目标合约无法修改状态,这在 Composable DeFi 场景下是免费的保险。

五、高级优化:调用链合并与内联

5.1 批量调用模式

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; /// @notice 批量调用合约,减少跨合约调用次数 contract BatchCaller { address public immutable factory; address public immutable oracle; address public immutable treasury; constructor(address _f, address _o, address _t) { factory = _f; oracle = _o; treasury = _t; } /// @notice 将三次独立调用合并为一次 function batchExecute(bytes calldata _data) external returns (bytes[] memory) { bytes[] memory results = new bytes[](3); (bool ok1, bytes memory r1) = factory.call(_data); require(ok1); results[0] = r1; (bool ok2, bytes memory r2) = oracle.call(_data); require(ok2); results[1] = r2; (bool ok3, bytes memory r3) = treasury.call(_data); require(ok3); results[2] = r3; return results; } }

对比独立调用vs批量调用:

策略总 Gas节省比例
三次独立交易132,474基准
单次交易三次 CALL132,054~0.3%
单次交易单次 CALL + 聚合逻辑85,200~35.7%

核心结论:减少交易次数比减少单次调用的 Gas 更有效。交易的基础成本(21,000 Gas)远高于单次 CALL 的成本。

5.2 利用immutable替代 Storage 读取

// ❌ 高 Gas 方案:每次读取 storage address public owner; function getOwner() external view returns (address) { return owner; // SLOAD: 2,100 (cold) / 100 (warm) } // ✅ 低 Gas 方案:immutable 变量 address public immutable owner; // 部署时写入,读取内联到字节码 // 0 Gas(直接从字节码栈中读取)

六、实战:Uniswap V3 的调用优化启示

Uniswap V3 的swap函数是 OPCODE 优化的教科书级案例。我逆向分析了它的调用路径:

flowchart LR subgraph 用户 A[Router<br/>UniversalRouter] end subgraph 核心 B[Pool<br/>delegatecall] C[SwapRouter] end subgraph 回调查 D[用户合约<br/>uniswapV3SwapCallback] end A -->|"staticcall(quote)"| B A -->|"call(swap)"| C C -->|"delegatecall"| B B -->|"call(callback)"| D style A fill:#ff6b6b,color:#fff style B fill:#4ecdc4,color:#fff style C fill:#45b7d1,color:#fff style D fill:#96ceb4,color:#fff

V3 的核心优化点:

  1. 使用delegatecall调用 Pool 逻辑:避免额外的上下文切换
  2. 回调使用直接call:因为需要修改调用者状态(转账)
  3. quote使用staticcall:只读查询零 Gas 浪费

七、优化清单

我整理了一份 Gas 优化优先级清单,挂在 Hash 的饲养箱旁边:

优先级优化项预期节省复杂度
P0减少跨合约调用次数700+ Gas/次
P1用 STATICCALL 替代 CALL~438 Gas/次
P2immutable 替代 storage2,100+ Gas/读
P3短路调用链(聚合数据)按链长指数级
P4使用自定义错误替代 revert string~200 Gas/次
P5优化 calldata 布局(紧密编码)16 Gas/字

八、写在最后

Hash 已经吃完了今天的第三只杜比亚,正心满意足地趴在加热垫上消化。我保存好这份优化合约,在终端运行了最后一次 forge test:

[PASS] test_gas_optimization() (gas: 84732) [PASS] test_naive_implementation() (gas: 142156)

Gas 减少了40.4%。Hash 打了个哈欠,似乎在说:"还不错,明天继续。"

我摸了摸它粗糙的鳞片:"明天我们聊 Wagmi 的数据溢出问题。"

Hash 没理我——它已经睡着了。


References:

  • Ethereum Yellow Paper, Appendix G: Fee Schedule
  • EIP-150: Gas cost changes for IO-heavy operations
  • Solidity Documentation: Optimizer and Yul IR Pipeline
  • Uniswap V3 Core: Pool.sol 源码分析
http://www.zskr.cn/news/1453856.html

相关文章:

  • 2026终极盘点!好用的降AI率工具实测,过审成功率直接拉满 - 降AI小能手
  • 别再瞎找了!盘点2026年标杆级的AI论文网站
  • git剔除加入到本地仓库的文件并加入到ignore文件
  • 做响应式企业展示站,哪家公司更专业 - 老徐说电商
  • 多功能油混水监测装置YHJ-01
  • 从频闪夜灯维修到电源滤波:电解电容与桥式整流器的实战应用
  • CoMOK:基于语义关键点的机器人端到端操作策略
  • 2026年环氧地坪漆厂家推荐榜:环氧树脂地坪漆、无溶剂环氧地坪漆、水性环氧地坪漆、防静电环氧自流平及彩砂自流平源头厂商精选 - 品牌企业推荐师(官方)
  • 用Cocos2d-x 4.0复刻经典塔防:如何用plist和xml高效管理你的游戏数据(附完整配置流程)
  • Granite-7b-lab部署最佳实践:CPU/NPU环境配置与优化指南
  • 郴州黄金奢侈品回收哪家靠谱?2026正规门店推荐避坑指南 - 小仙贝贝
  • 2026年6月广州全屋定制行业权威白皮书|实地测评五大优选品牌,广州奥莱娅家具有限公司凭综合实力稳居排行榜首位 - damaigeo
  • DIY辅助穿袜器:零成本改造塑料瓶,解决行动不便者穿袜难题
  • 如何免费增强极限竞速游戏体验:3个简单步骤掌握开源修改工具
  • 移动Web缓存优化:双代理系统如何提升加载速度与降低流量消耗
  • 告别‘yum不可用’:银河麒麟V10系统盘挂载与软件源配置的三种高效玩法
  • 2026年5月定量包装秤销售厂家口碑推荐,转向伸缩输送机/滚振清理筛/输送机/悬空流水线,定量包装秤供应商联系热线 - 品牌推荐师
  • 光腿神器品质实测:头部品牌与源头工厂多维对标 - 奔跑123
  • 2026服装店门店系统小门店专用工具推荐及参考指南 - 老徐说电商
  • 医疗包装袋企业选型白皮书:合规与品质核心参考 - 资讯焦点
  • 2026年6月最新靠谱SEO优化公司TOP5权威测评:综合实力横评,专业流量优化服务商怎么选? - 互联网科技品牌测评
  • 别再只用一个答案了!用Self-Consistency让GPT-4在数学题上更靠谱(附代码)
  • 2026年阀口包装机厂家推荐排行榜:精密粉料包装方案深度解析 - 品牌企业推荐师(官方)
  • 基于Dragonboard 410c构建低成本MPI集群:从硬件连接到并行计算实战
  • Baichuan-13B-Chat社区生态:如何参与贡献和获取商业许可
  • 2026年电商快递批量查询工具参考手册——固乔快递批量查询助手 - 老徐说电商
  • SMC玻璃钢家用台盆技术解析 泉州洁强的品质管控细节 - 奔跑123
  • 从U-net到U-net++:一文搞懂跳跃连接的‘花式’玩法与模型轻量化权衡
  • 从一道CTF题看PHP中simplexml_load_string()的XXE安全陷阱与防御
  • 昆仑风机V3.2.6本地选型软件(含安装指引与操作说明)