news 2026/9/17 3:36:44

Foundry 静态检查规则 unchecked-call:杜绝被忽略的低层调用返回值

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Foundry 静态检查规则 unchecked-call:杜绝被忽略的低层调用返回值

Foundry 静态检查规则 unchecked-call:杜绝被忽略的低层调用返回值

【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry

低层调用(low-level call)是 Solidity 开发中最危险的模式之一:被调用方失败时调用不会 revert,而是静默返回false。Foundry 内置的forge lint静态检查器通过unchecked-call(High 严重级别)规则,在编译期定位所有丢弃了成功标志的低层调用,帮助开发者在部署前拦截这类高危隐患。本文以 crates/lint/docs/unchecked-call.md 为主线,结合其源码实现(crates/lint/src/sol/high/unchecked_calls.rs)与测试用例(crates/lint/testdata/UncheckedCall.sol),讲清规则判定边界、运行方式与修复方案。

规则概览:ID、严重级别与检测目标

unchecked-call是 Foundry 内置 Solidity 检查器forge lint提供的一条 High 级别规则,其元信息定义如下:

属性
规则 IDunchecked-call
严重级别(Severity)High
检测目标低层调用返回的bool成功标志被丢弃
告警消息low-level call does not check the success return value

在源码中,该规则通过declare_forge_lint!宏注册,见 crates/lint/src/sol/high/unchecked_calls.rs:

declare_forge_lint!( UNCHECKED_CALL, Severity::High, "unchecked-call", "low-level call does not check the success return value" );

它与erc20-unchecked-transfer(同样为 High 级别的 ERC20 返回值检查规则)一同在 crates/lint/src/sol/high/mod.rs 中注册:

unchecked_calls: (UncheckedCall, early, (UNCHECKED_CALL)), (UncheckedTransferERC20, late, (ERC20_UNCHECKED_TRANSFER));

其中UncheckedCall注册为EarlyLintPass(在 HIR 之前基于 AST 直接判定),而UncheckedTransferERC20是 LateLintPass,两者侧重点不同:前者只管低层调用的成功标志,后者负责 ERC20transfer/transferFrom的返回值。

规则触发条件(What it does)

规则在以下两种情况下告警:

  1. 低层调用的返回值整体被丢弃——调用以独立表达式语句形式出现,例如target.call(data);
  2. 只解构保留了bytes memory返回数据——例如(, bytes memory ret) = target.call(data);,只取第二个元素而放弃第一个bool success

这里的"低层调用"特指 Solidity 的三种 address 成员方法:

  • addr.call(...)
  • addr.delegatecall(...)
  • addr.staticcall(...)

均支持{value: x}/{gas: g}选项形式,判定函数位于 crates/lint/src/sol/analysis/exprs.rs:

/// AST-level: `target.call(...)`, `.delegatecall(...)`, `.staticcall(...)`, with or without /// `{value: x}` options. pub const fn is_low_level_call(expr: &ast::Expr<'_>) -> bool { if let ast::ExprKind::Call(call_expr, _) = &expr.kind { let callee = match &call_expr.kind { ast::ExprKind::CallOptions(inner, _) => inner, _ => call_expr, }; if let ast::ExprKind::Member(_, member) = &callee.kind { return matches!(member.name, kw::Call | kw::Delegatecall | kw::Staticcall); } } false }

注意transfersend不在其列——二者虽然也是低层交互,但失败时会自动 revert(transfer)或返回值本身就是boolsend),不会被此规则误报(详见下文"判定边界")。

为什么危险:静默失败与状态污染

文档明确给出了这条规则存在的原因:

Low-level calls donotrevert when the callee fails; they silently returnfalse. Ignoring the success flag means a failed call is indistinguishable from a successful one, leading to bugs where state is updated on the assumption that an external interaction succeeded.

与普通的外部函数调用(如接口调用IToken(token).transfer(...))不同,低层调用把错误处理完全交给调用方:被调用合约 revert、目标地址没有代码、gas 耗尽等任何失败情形,都只表现为返回(false, bytes memory)不会向上传播异常

因此,一旦忽略这个bool,程序就无法区分"调用成功"与"调用失败",常见的连锁后果包括:

  • call之后无条件更新状态变量,把"从未发生的外部操作"当作"已生效"记账,造成账实不符;
  • 依赖回调结果(如delegatecall写入的存储、staticcall读取的返回值)继续执行后续逻辑,基于错误数据做决策;
  • 资金转移场景下,ETH 没有真正转出,但代码继续执行"转账后"的收尾逻辑。

一句话概括:丢弃成功标志 = 对失败完全无感。这正是unchecked-call被定为 High 严重级别的原因。

违规示例与推荐修复

原文档给出的违规写法:

target.call(data); // success ignored (, bytes memory ret) = target.call(data); // only payload kept

第一行完全忽略了返回值;第二行虽然解构了元组,但只保留了bytes载荷,真正关键的成功标志bool,直接跳过。

推荐的修复方式是把成功标志显式解构出来并强制检查:

(bool ok, ) = target.call(data); require(ok, "call failed");

进一步地,还可以利用 Solidity 的require短路特性把检查写得更紧凑,并配合自定义错误节省 gas:

(bool ok, bytes memory ret) = target.call{value: amount}(data); if (!ok) revert CallFailed(); // 之后才可安全使用 ret 或更新状态

修复的核心原则是:在使用调用结果(返回值、回调副作用、后续状态写入)之前,必须让bool success参与一次显式检查requireassertif (!ok) revert或直接return success)。

判定边界:什么会报、什么不会报

规则的实现是严格的 AST 模式匹配。从 crates/lint/src/sol/high/unchecked_calls.rs 可以精确还原判定逻辑:

impl<'ast> EarlyLintPass<'ast> for UncheckedCall { fn check_stmt(&mut self, ctx: &LintContext, stmt: &'ast Stmt<'ast>) { let span = match &stmt.kind { // `target.call(data);` 和 `(, existingVar) = target.call(data);` StmtKind::Expr(expr) if is_low_level_call(expr) || matches!(&expr.kind, ExprKind::Assign(lhs, _, rhs) if is_low_level_call(rhs) && matches!(&lhs.kind, ExprKind::Tuple(elements) if elements.first().is_none_or(|e| e.is_none()))) => { expr.span } // `(, bytes memory data) = target.call(data);` StmtKind::DeclMulti(vars, expr) if is_low_level_call(expr) && vars.first().is_none_or(|v| v.is_none()) => { stmt.span } _ => return, }; ctx.emit(&UNCHECKED_CALL, span); } }

据此可以总结出完整的判定边界:

代码形态是否告警原因
target.call(data);✅ 告警表达式语句,返回值整体丢弃
target.call{value: v}("");✅ 告警{value}选项同样匹配is_low_level_call
target.delegatecall(data);✅ 告警同属低层调用集合
target.staticcall(data);✅ 告警同属低层调用集合
(, bytes memory d) = target.call("");✅ 告警DeclMulti首元素为None,只保留载荷
(, existingVar) = target.call("");✅ 告警赋值到既有变量的元组,首元素同样被丢弃
(bool ok, ) = target.call(data);❌ 不告警成功标志被解构,且后续被检查
(bool ok, ) = target.call(data); require(ok, ...);❌ 不告警require/assert/if (!ok) revert均为合法检查
target.transfer(1 ether);/target.send(1 ether);❌ 不告警transfer自动 revert、send返回bool,二者都不是"静默失败"的低层调用
(bool ok, bytes memory d) = target.call(data);随后return d;(未用 ok)不告警规则只判定"成功标志是否被解构出来",不做数据流分析——解构但未使用不会报

最后一行是需要开发者注意的边界:该规则是语法级(Early/基于 AST)而非数据流级检查,它只保证bool被解构出来,不追踪它后续是否真的参与了分支判断。这是它的设计取舍,也意味着完全依赖单条规则并不能覆盖所有"解构了却不检查"的情况,建议与代码评审、forge test的测试覆盖配合使用。

测试用例 crates/lint/testdata/UncheckedCall.sol 完整覆盖了上述边界:checkedCallWithTuplecheckedCallWithIfStatementcheckedDelegateCallcheckedStaticCallcheckedCallInRequirecheckedCallWithAssertcheckWithExistingVarsendEther全部标记为 SHOULD PASS,而 8 个 SHOULD FAIL 用例(含uncheckedCallWithValuemultipleUncheckedCallsignoredReturnWithPartialTupleignoredReturnExistingVar)分别对应//~WARN:断言,期望输出见 crates/lint/testdata/UncheckedCall.stderr。

告警输出格式

命中规则时,forge lint输出如下(摘自测试基线 crates/lint/testdata/UncheckedCall.stderr):

warning[unchecked-call]: low-level call does not check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC │ LL │ target.call(data); │ ━━━━━━━━━━━━━━━━━ │ ╰ help: https://getfoundry.sh/forge/linting/unchecked-call

输出包含规则 ID、告警消息、精确到列的位置信息与源代码行高亮。该规则属于Severity::High,对应终端输出为红色加粗(Severity::color实现见 crates/config/src/lint.rs),方便在大量告警中快速定位高危项。

在项目中启用与运行

unchecked-call属于默认启用的 High 级别规则,无需任何额外配置即可工作。项目默认的 lint 配置(见 crates/config/src/lint.rs)为:

  • lint_on_build: true构建时自动执行 lint(可通过配置关闭)
  • severity: [High, Med, Low]:默认启用高、中、低三个级别,unchecked-call处于 High,天然在列
  • exclude_lints: []:不排除任何规则

因此常规的forge build就会报告unchecked-call告警;也可以使用独立命令针对性检查:

# 只运行 unchecked-call 单条规则 forge lint --only-lint unchecked-call # 指定文件/目录(覆盖 ignore 配置) forge lint src/Contract.sol # 按严重级别过滤 forge lint --severity high # 输出 rustc 兼容的 JSON 格式(配合 CI 解析) forge lint --format-json

--only-lint--severity的优先级逻辑见 crates/forge/src/cmd/lint.rs:显式传入--only-lint时会绕过严重级别过滤,只运行指定的规则 ID。另外需要注意,lint 依赖 Solar 编译器,仅支持 Solidity >= 0.8.0的源码(见 crates/forge/src/cmd/lint.rs)。

foundry.toml 配置

foundry.toml[lint]段中可以持久化配置:

[lint] # 只运行高、中、低三个级别(默认值) severity = ["high", "med", "low"] # 排除指定规则(按 ID),例如想保留 unchecked-call 而排除其他 exclude_lints = ["mixed-case-function"] # 忽略的 glob 路径 ignore = ["test/**", "script/**"] # 构建时是否自动 lint,默认 true lint_on_build = true

LinterConfig的完整字段定义见 crates/config/src/lint.rs。此外,根据 crates/lint/README.md,位于 test 与 script 目录下的文件默认会被排除在所有 lint 之外(unsafe-cheatcodeenvironment-read-across-mutation两条除外),即测试辅助代码中的低层调用不会触发本规则,生产源码则始终接受检查。

行内抑制

对于确认为有意为之的调用,可以在语句前添加行内抑制注释(该语法同样适用于所有 lint 规则):

// forge-lint: disable-next-line(unchecked-call) (bool ok, ) = target.call(data); // ok 在本函数末尾统一校验

如果某条抑制注释没有实际命中任何告警,可配合--report-unused-suppressions将其暴露出来:

forge lint --report-unused-suppressions

使用说明见 crates/forge/src/cmd/lint.rs。

实现要点小结

  • 规则定位unchecked-callforge lint内置的 High 级别静态检查规则,ID 为unchecked-call,注册为 EarlyLintPass,基于 AST 语法匹配,见 crates/lint/src/sol/high/unchecked_calls.rs。
  • 覆盖范围calldelegatecallstaticcall三种低层调用(含{value}/{gas}选项),覆盖"独立表达式丢弃返回值"与"元组解构只取bytes"两类形态。
  • 修复范式(bool ok, ) = target.call(data);后立即require(ok)/assert(ok)/if (!ok) revert,或直接return ok
  • 验证途径:仓库测试基线 crates/lint/testdata/UncheckedCall.sol 与 crates/lint/testdata/UncheckedCall.stderr 给出了 8 个命中与 8 个放行用例,是理解判定边界的最佳参照。

unchecked-call纳入日常开发与 CI 流水线,能以接近零成本的方式消除一类极易被忽视却后果严重的合约缺陷——任何低层调用,都应该给失败一个"说得出口"的去处。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 3:35:55

T113-S3 Linux移植实战:从BootROM到根文件系统的全链路调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 3:35:19

聚合广告SDK深度解析:从Waterfall到Bidding的变现优化指南

做移动应用变现这些年&#xff0c;我一直有个很深的感受&#xff1a;很多人以为把广告SDK接进来就能躺着赚钱&#xff0c;结果一个App接了一家广告平台&#xff0c;填充率低得可怜&#xff0c;eCPM也上不去&#xff0c;折腾一圈收益还不如预期。后来换成了聚合广告SDK&#xff…

作者头像 李华
网站建设 2026/9/17 3:34:25

无锡海顿壁挂炉维修电话|漏水故障预约检修|欧米到家咨询电话

文章简介无锡冬季湿冷明显&#xff0c;壁挂炉承担家庭洗浴热水、地暖、暖气片采暖等多项需求&#xff0c;设备运行时间长、启停频率高&#xff0c;容易出现不点火、点火后熄火、热水忽冷忽热、地暖升温慢、暖气片局部不热、运行反复掉压、接口漏水、异响报警、频繁启停等问题。…

作者头像 李华
网站建设 2026/9/17 3:33:20

蓝牙音箱选购指南:场景化推荐与避坑技巧

我这几年摸过的蓝牙音箱没有五十款也有三十款&#xff0c;从几十块的工包货到几千块的桌面旗舰都听过一轮。每次写推荐总有朋友问“到底买哪款”&#xff0c;其实这个问题的标准答案永远是&#xff1a;先搞清楚你在哪用、听什么、预算多少。这篇文章不搞玄学&#xff0c;直接按…

作者头像 李华
网站建设 2026/9/17 3:27:58

Temu商家必看:用凌风工具批量修正体积重量,物流成本立省

很多做Temu的商家&#xff0c;尤其是半托管和本对本模式的卖家&#xff0c;应该都有过这种经历&#xff1a;货发出去了&#xff0c;后台结算单出来一看&#xff0c;物流费比自己预估的高出一大截。去查明细&#xff0c;十有八九是栽在“体积重量”这四个字上。不是申报的时候填…

作者头像 李华