尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Foundry 合约 Gas 优化:`forge lint` 的 `var-read-using-this` 规则深度解读

Foundry 合约 Gas 优化:`forge lint` 的 `var-read-using-this` 规则深度解读 Foundry 合约 Gas 优化forge lint的var-read-using-this规则深度解读【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry在 Solidity 合约中通过this.foo()调用自身合约的public状态变量 getter 或view/pure函数是一种常见但代价高昂的写法它会在 EVM 层面编译成一次指向合约自身地址的STATICCALL白白消耗 Gas。Foundry 内置的 Solidity 静态分析器forge lint提供了var-read-using-this规则来定位这类冗余外部调用并给出可直接落地的修复建议。本文以 crates/lint/docs/var-read-using-this.md 为骨架结合仓库中该规则的完整实现与测试用例讲解其检测范围、触发场景、自动修复逻辑与正确的规避姿势帮助开发者在日常合约开发中低成本地压掉这部分 Gas 开销。规则概览检测什么、为什么值得修What it does检测范围var-read-using-this规则报告通过this对合约自身的public状态变量 getter以及view或pure函数的调用——包括从父合约继承来的函数。典型触发代码形如contract C { uint256 public counter; mapping(uint256 address) public owners; function readDirect() external view returns (uint256, address) { // Each this.X performs an unnecessary STATICCALL. return (this.counter(), this.owners(0)); } }建议改为直接读取状态contract C { uint256 public counter; mapping(uint256 address) public owners; function readDirect() external view returns (uint256, address) { // Direct storage reads — no external call. return (counter, owners[0]); } }针对public状态变量 getter替换原则是“直接读状态”用foo代替this.foo()用m[k]代替this.m(k)。但替换前必须确认两点新写法是否保持了 getter 的返回值语义以及是否刻意保留了外部调用行为。Why is this bad每次调用都是一次 STATICCALL每次this.X(...)调用都会编译为对合约自身地址的一次STATICCALL。其开销构成包括STATICCALL本身的固定 Gas 开销调用参数与返回数据的 ABI 编码/解码开销再加上存储读取本身的成本。也就是说为了读取一个本可以直接访问的存储变量白白付出了一次完整的外部调用的代价。直接读取变量则完全跳过这次调用属于零成本且更清晰的写法。深入实现规则在源码中如何工作该规则实现在 crates/lint/src/sol/gas/var_read_using_this.rs通过declare_forge_lint!宏注册声明信息如下declare_forge_lint!( VAR_READ_USING_THIS, Severity::Gas, var-read-using-this, call through this to a view or pure function incurs a STATICCALL );规则注册于 crates/lint/src/sol/gas/mod.rs属于late 阶段的 lint passlate表示在类型检查后的 HIR 上运行。从源码结构可以梳理出该规则的完整判定流程1. 仅作用于有this的合约类型check_nested_contract首先检查合约类型只有Contract普通合约和AbstractContract抽象合约才存在this语义——library 没有thisinterface 没有函数体两者直接跳过。测试文件 crates/lint/testdata/VarReadUsingThis.sol 中也包含一个abstract contract AbstractCase证实抽象合约同样会被检测。2. 遍历范围不止函数体遍历覆盖两类位置状态变量初始化器运行在编译器合成的构造函数中当前合约自己定义的函数体继承来的函数体由各自的合约遍历避免重复报告。测试中专门覆盖了状态变量初始化器场景// State-variable initializer is walked too (runs in the synthesized constructor). uint256 public initFromThis uint256(uint160(address(this))) this.counter();同时由于遍历器基于 HIR 的Visittrait 实现修饰符modifier调用参数也会被走到——测试中的gatedWithArg用例证实了这一点这是仅遍历函数体的朴素实现容易漏掉的情况。3. 命中条件成员调用 this view/purecheck_call的核心判定逻辑为表达式必须是调用ExprKind::Call(callee, args, opts)callee 去掉括号后必须是成员访问ExprKind::Member(base, member)base解析后必须是内置的this通过is_builtin(..., sym::this)判断因此(this).counter()、(this.counter)()这类带括号写法同样能识别见测试用例成员解析到的函数含继承函数的state_mutability必须是View或Pure。非 view/pure 的调用不会触发。例如测试中的this.publicMutating()修改状态的public函数不报告——这种通过this发起的状态修改调用虽然也有 Gas 开销但属于不同的语义问题不在本规则范围内。4. 自动修复suggestion的生成策略suggestion函数根据目标函数类别给出三类不同的建议目标函数建议动作修复类型普通view/pure函数非 getter提示call directly without \this.: name(...)仅示例example无自动改写无参状态变量 getter直接替换为变量名如counter可机器应用MachineApplicable带参 gettermapping/数组替换为下标访问如owners[k]、balances[a][b]可能不正确MaybeIncorrect需人工复核测试的 stderr 输出 crates/lint/testdata/VarReadUsingThis.stderr 中可以看到对应的帮助信息例如note[var-read-using-this]: call through this to a view or pure function incurs a STATICCALL LL │ return this.owners(k); │ ━━━━━━━━━━━━━━ help: consider accessing storage directly: owners[k]对于 mapping/数组 getter修复代码会把每个实参转成下标拼接name[arg1][arg2]...嵌套 mapping 如balances[a][b]也能正确生成但因为实参表达式需要被包进下标存在求值顺序等语义风险所以标记为MaybeIncorrect。5. 刻意保留外部调用的场景不提供自动修复当调用带显式调用选项如{gas: 10000}时外部调用是开发者的刻意行为规则仍然报告提示 Gas 浪费但不生成自动修复对应源码中的if opts.is_some() { None }分支。测试用例function withCallOptions() external view returns (uint256) { return this.publicViewLocal{gas: 10000}(); }6. try 的目标除外try this.X()在 Solidity 语法层面必须是外部调用因此try的目标表达式被显式排除遍历器维护try_target字段。不过try 的返回值处理块内部或try 参数中嵌套的其他this.X(...)仍会被报告测试中的tryWithNestedRead用例验证了这一边界。边界情况与豁免清单哪些不会被误报结合 crates/lint/testdata/VarReadUsingThis.sol 的 SHOULD PASS 部分与源码注释以下情况不会触发规则函数引用而非调用return this.publicViewLocal;只取函数指针不执行调用其他合约other.value()base 不是this可变函数this.publicMutating()mutability 非 view/pure内部函数直呼internalView()不经thissuper调用super.basePublicView()delegatecall 机制不是this库调用Lib.foo()try this.X()外层目标必须保持外部调用显式地址转换Other(address(this)).value()显式接口转换不追踪内置地址成员address(this).balance非函数调用函数指针成员访问this.publicViewLocal.selector内联抑制// forge-lint: disable-next-line(var-read-using-this)。此外测试还覆盖了同参数数量的重载函数场景this.ambiguous(0)会按被选中重载的 mutability判定view重载命中即报告而this.ambiguous(address(0))命中可变重载则不报告。值得注意的还有两点语义细节原文档明确指出struct getter 返回的是选定字段而非整个结构体直接读状态变量语义不等价此时只给出示例性提示如read the state variable directly: \info不做机器自动改写局部变量可能遮蔽状态变量直接替换前需确认名字解析没有变化。针对 external view/pure 函数的特别说明对于external view/pure函数情况比较特殊从合约内部通过this调用它们是唯一能解析成功的合约内语法直接用函数名调用external函数在 Solidity 中是不允许的。此时规则同样会报告但自动修复无法直接改写——因为没有等价的直接调用语法。原文档给出的推荐方案是重构将函数体抽取到一个internal辅助函数中让external入口与本地调用方共同调用它。例如contract C { uint256 public counter; function externalViewLocal() external view returns (uint256) { return _readCounter(); } // internal helper: both the external entry point and local callers use it function _readCounter() internal view returns (uint256) { return counter; } function callExternalView() external view returns (uint256) { return _readCounter(); // no STATICCALL } }源码中对该场景生成的是示例型建议Suggestion::example描述为 avoid theSTATICCALLby invoking the function directly提示开发者以去this.的方式直接调用但实际落地需要上述 internal 抽取。测试注释中也点明这是相对同类工具如 Slither的一个改进点external view/pure 函数经由this的调用同样会被标记。如何在项目中使用该规则通过 forge lint 运行在 Foundry 项目中运行forge lint只会对生产源码目录src等执行测试与脚本目录默认被排除。若只想查看本规则可以使用 lint 筛选参数测试文件头部即使用了--only-lint var-read-using-this编译标志对应交互命令形式forge lint --only-lint var-read-using-this该规则属于Gas严重级别。forge lint支持按严重级别筛选例如仅运行 Gas 级别规则。相关的配置选项可参考 crates/lint/README.md 中的SolidityLinter配置表with_severity按High/Med/Low/Info/Gas/CodeSize过滤、with_lints显式指定要包含的 lint、without_lints显式排除等。内联抑制如果某处this.X()是刻意为之例如测试代码、或确实需要外部调用语义可用行内注释抑制function suppressed() external view returns (uint256) { // forge-lint: disable-next-line(var-read-using-this) return this.counter(); }这是测试文件中验证过的语法按 lint ID 精确抑制。运行规则自带测试仓库为每条 lint 规则配套了触发/不触发的测试样例。运行cargo test -p forge-lint即可验证包括var-read-using-this在内的全部规则行为。其中 crates/lint/testdata/VarReadUsingThis.sol 定义了数十个触发//~NOTE:标注与豁免用例crates/lint/testdata/VarReadUsingThis.stderr 是期望的诊断输出二者共同锁定了规则的行为契约也是理解各类边界情况的最佳学习材料。总结var-read-using-this是 Foundry 内置 linter 中成本收益比极高的一条 Gas 优化规则检测逻辑清晰this view/pure 成员调用、误报边界定义完善try、调用选项、struct getter、重载、遮蔽等均有处理、修复建议分档合理可机器应用 / 需人工复核 / 仅示例提示。对合约开发者而言把代码中零散的this.counter()、this.owners(k)这类写法批量替换为直接存储访问就能在不改变任何业务语义的前提下稳定削减每次调用的固定 Gas 开销——在循环、修饰符、事件参数等高频执行路径上收益尤其明显。延伸阅读规则权威文档crates/lint/docs/var-read-using-this.md规则实现源码crates/lint/src/sol/gas/var_read_using_this.rs规则注册处crates/lint/src/sol/gas/mod.rs完整测试用例crates/lint/testdata/VarReadUsingThis.sol、crates/lint/testdata/VarReadUsingThis.stderr全部 lint 规则清单与配置说明crates/lint/README.mdlint 文档规范crates/lint/docs/README.md【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表