Foundry 规则深度解析:divide-before-multiply 先除后乘精度丢失检测
2026/9/16 18:05:15 网站建设 项目流程

Foundry 规则深度解析:divide-before-multiply 先除后乘精度丢失检测

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

divide-before-multiply是 Foundry 内置 Solidity 静态检查器(forge lint)中的一条**中危(Med)**规则,用于拦截(a / b) * c这类"先整数除法、再乘法"的表达式。由于 Solidity 的整数除法向零截断,这种写法会在放大之前就丢弃余数,是 DeFi 合约中手续费、份额、收益计算精度偏差的常见来源。读完本文,你将理解该规则的触发原理、修复写法与溢出权衡,并通过仓库源码与测试用例掌握其检测边界,能在foundry.toml中正确启用并落地到自己的合约工程中。

规则概览:它检测什么

该规则的完整定义为(见 crates/lint/src/sol/med/div_mul.rs):

属性
规则 IDdivide-before-multiply
严重级别Med(中危)
触发形态(a / b) * c及其等价写法
警告信息division before multiplication may lose precision

从源码实现看,declare_forge_lint!宏将该规则注册为LateLintPass(在语义分析阶段之后运行),入口为check_function——即对每个函数的函数体进行逐语句、逐表达式的检查,而不是简单的正则或文本匹配。这意味着它理解 Solidity 的语法与类型,能识别括号、复合表达式、赋值乃至内联汇编中的真实除法/乘法语义。

为什么这是坏的:整数除法的精度陷阱

Solidity 的整数除法向零截断。对表达式(a / b) * c,求值顺序是:先执行a / b得到截断后的商,丢弃余数,再乘以c放大;而(a * c) / b先在整数域内完成缩放再做除法,保留了更多精度。

以文档中的示例为例:

// 先除后乘:截断发生在缩放之前 uint256 share = (amount / total) * weight; // 先乘后除:保留精度 uint256 share = (amount * weight) / total;

amount = 100total = 3weight = 2

  • (100 / 3) * 2 = 33 * 2 = 66——100 / 3先截断为 33,损失了1/3
  • (100 * 2) / 3 = 200 / 3 = 66——仍会截断,但截断发生在最终一步,中间没有二次精度损失。

两种写法在数值上可能恰好相等,也可能相差巨大,取决于操作数关系。仓库测试用例 DivideBeforeMultiply.sol 中覆盖了多种形态,例如(1 / 2) * 33 * (1 / 2)4 * ((1 + 2) / 3),以及分母中出现"先乘后除"的1 / ((2 / 3) * 3)也会被警告——因为被乘数/除数本身就是除法结果时同样存在精度损失。

在实际业务中,该模式常体现为手续费计提、代币份额分配、收益率换算等场景的系统性小数偏差,日积月累可被利用或导致账目失衡。

修复写法与溢出权衡

文档明确指出:先乘后除并不总是安全的(a * c) / b中的a * c可能溢出,即使最终结果恰好适配当前整数类型。因此选择重写时需要明确两点:

  1. 确认乘积不溢出:当a * c的中间乘积能安全放入该整数类型(如uint256)时,直接改写为先乘后除;
  2. 否则使用全精度辅助函数:改用检查式乘法/除法(checked 乘法,或专门的"全精度乘法-除法"库函数),如 OpenZeppelin 风格的mulDiv实现;
  3. 显式决定舍入行为:计算需要向零舍入、向上舍入还是四舍五入,应根据业务语义显式选择,而不是依赖求值顺序偶然决定的截断。

也就是说,规则的意图不是机械地要求"一律先乘后除",而是提醒开发者对除法后的乘法保持警惕,并显式决策精度与舍入策略

从源码看检测能力:污点数据流分析

该规则的实现远比"匹配(a / b) * c文本"强大。核心是一个污点分析(taint analysis):以Tainted = HashSet<VariableId>记录"当前值来自除法结果"的局部变量集合,然后沿控制流传播(见 div_mul.rs)。

污点的设置与传播

  • 变量声明uint256 q = a / b;q被标记为污点;q = a + b;清除污点(div_mul.rs)。
  • 普通赋值:右值如果是除法或污点变量,左值继承污点;元组解构赋值((x, y) = (a / b, c))会逐元素判定,而不是整体污染(rhs_taints函数,见 div_mul.rs)。
  • 复合赋值q *= cq带污点则直接告警;q /= b会重新设置污点;而q += 1q++等会清除污点(见 div_mul.rs)。测试函数compoundClearsTaintincrementClearsTaint验证了这一点。
  • 别名与复制uint256 copy = q; return c * copy;污点会随变量复制继续传播。

控制流感知

污点并非简单地"沾上就不掉",而是按分支、循环做保守合并

  • if 分支:任一可达分支落下时,合并各分支的污点(check_branches,见 div_mul.rs)。测试中branchPropagated(条件分支内赋值后使用)会告警。
  • return / revert 分支不泄漏:若某分支以returnrevert终止,则该分支内的污点不会污染后续代码——对应测试returningBranchDoesNotLeakrevertingBranchDoesNotLeak均不告警。
  • 循环:循环体内产生的污点会传播到循环后使用(loopPropagated告警)。
  • try/switch:同样按互斥分支合并污点。
  • 函数调用参数:调用并不会把普通参数"宽泛地"视为除法结果(noBroadRhsTaint测试验证了helper(a / b)不会污染helper(q)之后的乘法)。

这种设计使规则既能发现跨语句、跨分支的精度问题,又避免了对无关代码的误报。

内联汇编(Yul)检测

通过is_yul_call识别 Yul 内建函数,规则同样覆盖assembly块(见 div_mul.rs):

assembly { result := mul(div(a, b), c) // 告警:div 后 mul result := mul(c, sdiv(a, b)) // 告警:sdiv(有符号除法)后 mul let q := div(a, b) result := mul(q, c) // 告警:污点变量参与 mul }

有符号除法sdiv与无符号div都会被跟踪;测试文件通过--only-lint divide-before-multiply custom-errors inline-assembly编译标志同时开启了inline-assembly规则以验证这一路径(见 DivideBeforeMultiply.sol)。

如何启用与使用

forge lint会在forge build的编译后阶段自动运行(lint_on_build,见 crates/forge/src/cmd/build.rs)。针对本项目,可执行:

# 仅运行本规则(配合其他规则过滤,参考测试用例的编译标志) forge lint --only-lint divide-before-multiply # 跳过编译后的 lint 步骤(相当于 foundry.toml 中 [lint] lint_on_build = false) forge build --no-lint

foundry.toml中可通过[lint]段控制(对应 build.rs 中severityexclude_lintslint_specific等配置的解析):

[lint] lint_on_build = true severity = ["warn"] # 至少以警告级别展示 exclude_lints = [] # 例如 ["divide-before-multiply"] 可排除本规则

注意:当前 lint 引擎(Solar)仅支持 Solidity >= 0.8.0的源码(见 build.rs),低于该版本的合约不会被此规则分析。

参考与深入阅读

  • 规则文档原文:crates/lint/docs/divide-before-multiply.md
  • 规则实现源码:crates/lint/src/sol/med/div_mul.rs
  • 规则注册(med级别模块):crates/lint/src/sol/med/mod.rs
  • 测试用例(正反例全集):crates/lint/testdata/DivideBeforeMultiply.sol
  • 测试期望输出(stderr 快照):crates/lint/testdata/DivideBeforeMultiply.stderr
  • 规则清单与说明:crates/lint/README.md
  • lint 引擎接入点(build 阶段):crates/forge/src/cmd/build.rs

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询