I almost reported a critical bug that didn't exist. One constant saved me.
I almost reported a critical bug that didn’t exist. One constant saved me.
我差点提交了一个根本不存在的“严重漏洞”,是一个常量救了我。
Last week I was reviewing the staking engine of a protocol before its mainnet launch. Deep in a 1,400-line contract, I found what looked like a serious bug. The reward math multiplied three values before dividing: uint256 delta = (lot.amount * rBase * midpointRate) / (RAY * RAY); Multiply-before-divide. If that intermediate product overflows uint256, the whole epoch settlement reverts — and since every stake, withdraw, and setStake runs it, the post’s funds get permanently frozen. That’s a High-severity, fund-locking DoS.
上周,我在一个协议主网上线前对其质押引擎进行审计。在 1,400 行代码的合约深处,我发现了一个看起来很严重的漏洞。奖励计算公式在除法前先进行了三次乘法:uint256 delta = (lot.amount * rBase * midpointRate) / (RAY * RAY);。这是典型的“先乘后除”。如果中间乘积超过了 uint256 的上限,整个纪元结算就会回滚——由于每次质押、提取和设置质押都会运行该逻辑,这会导致资金被永久锁定。这是一个高危的、会导致资金锁定的拒绝服务(DoS)漏洞。
I had the finding half-written. lot.amount can reach 10M tokens (1e25). rBase grows with elapsed time. midpointRate can hit RAY. Multiply those and you blow past 2^256… I was ready to send it. Then I did the one thing that separates a real audit from false-positive spam: I checked the actual constants before writing the claim.
我当时已经写好了一半的报告。lot.amount 可能达到 1000 万代币(1e25),rBase 会随时间增长,midpointRate 可能达到 RAY。将它们相乘就会溢出 2^256……我当时准备直接提交。但随后我做了一件事,这也是区分专业审计与垃圾误报的关键:在写报告前,我核对了实际的常量。
uint256 private constant RAY = 1e18; // I'd assumed 1e27
RAY was 1e18, not the 1e27 I’d been carrying in my head. And the interest rate had a hard cap — MAX_RATE_MAX_RAY = 5e18, enforced even against a fully-captured timelock. I ran the numbers with the real values: For that multiplication to overflow, the protocol would need to go 2.3 × 10¹⁵ years without a single state update. Not reachable. The bug didn’t exist. I deleted the finding.
uint256 private constant RAY = 1e18; // 我之前误以为是 1e27
RAY 的值是 1e18,而不是我脑海中一直以为的 1e27。此外,利率还有一个硬上限 MAX_RATE_MAX_RAY = 5e18,即使在时间锁(timelock)被完全控制的情况下也会强制执行。我用真实数值重新计算了一遍:要使该乘法溢出,协议需要 2.3 × 10¹⁵ 年不进行任何状态更新。这根本不可能发生。漏洞不存在,我删除了这条发现。
Why this matters more than the bug would have: If I’d sent that report, here’s what happens: the team’s engineer clones the repo, plugs in the real constants, and realizes in ten minutes that I flagged an overflow that can’t happen. Every other finding in my report now gets read with a raised eyebrow. My credibility — the entire product — is gone. This is the dirty secret of automated smart-contract auditing: the bottleneck isn’t finding issues. It’s not drowning the real ones in false positives. Anyone can run a scanner and paste 40 “criticals.” A team that has to triage 40 flags to find the 2 that matter will — correctly — stop trusting you. 为什么这件事比漏洞本身更重要?如果我提交了那份报告,结果会是这样:团队工程师克隆代码库,代入真实常量,十分钟内就会意识到我标记了一个不可能发生的溢出错误。我报告中其他所有的发现都会因此受到质疑。我的信誉——整个审计产品——就毁了。这就是智能合约自动化审计的“肮脏秘密”:瓶颈不在于发现问题,而在于不要让真正的漏洞淹没在误报中。任何人都可以运行扫描器并粘贴 40 个“严重漏洞”。如果一个团队必须在 40 个标记中筛选出 2 个真正重要的问题,他们理所当然会停止信任你。
The bar I hold: zero false positives on OpenZeppelin. Here’s a concrete, checkable standard I keep: my scanner runs clean — zero findings — across the entire OpenZeppelin contracts library. All 248 source files. If a tool can’t stay silent on the most-reviewed Solidity code on earth, it has no business flagging your code. It’s a claim I re-verify obsessively, because it’s easy to break. 我坚持的标准:在 OpenZeppelin 上实现零误报。我保持着一个具体且可验证的标准:我的扫描器在整个 OpenZeppelin 合约库(全部 248 个源文件)中运行必须是干净的,即零发现。如果一个工具连地球上被审查次数最多的 Solidity 代码都无法保持“安静”,那它就没有资格标记你的代码。我强迫症般地反复验证这一标准,因为它很容易被打破。
In fact, I recently caught it broken. My access-control detector had started flagging OZ’s ERC1967Utils.upgradeToAndCall as “unprotected” — but that function is internal. It isn’t externally callable; it can’t be an access-control bug (the auth lives in the public entry point that calls it). One rule — skip internal/private functions — and OZ was clean again.
事实上,我最近就发现它失效了。我的访问控制检测器开始将 OZ 的 ERC1967Utils.upgradeToAndCall 标记为“未受保护”——但该函数是 internal 的。它无法被外部调用,因此不可能是访问控制漏洞(权限验证存在于调用它的公共入口点中)。增加一条规则——跳过 internal/private 函数——OZ 就又恢复“干净”了。
A few more false-positive classes I’ve had to kill, each one a lesson:
- The virtual-shares + 1 isn’t “rounding up.” A correct ERC-4626 vault writes
totalSupply() + 10**offsetandtotalAssets() + 1— that’s the OZ inflation-attack mitigation. A naive detector reads the + 1 as ceil-rounding and screams “share inflation risk” on correct code. It would fire on almost every well-built vault. - A permissionless
emergencyWithdraw()that moves funds into internal claimable state isn’t a fund-sweep. The caller can’t redirect anything; it’s a keeper-resilience pattern. Flagging it wastes everyone’s time. - Auth written in assembly is still auth.
if iszero(eq(sload(admin), caller())) { revert }protects a function just as well asonlyOwner. Gas-optimized code isn’t unprotected code. Each of these looks like a bug to a pattern-matcher. None of them are. Knowing the difference is the job. 我不得不消除的几类误报,每一类都是一堂课: - 虚拟份额(virtual-shares)的
+ 1并不是“向上取整”。一个正确的 ERC-4626 金库会写入totalSupply() + 10**offset和totalAssets() + 1——这是 OZ 的通胀攻击缓解机制。幼稚的检测器会将+ 1误读为向上取整,并在正确的代码上大喊“存在份额通胀风险”。这几乎会在每一个构建良好的金库上触发报警。 - 将资金转移到内部可领取状态的无权限
emergencyWithdraw()并不是资金窃取。调用者无法重定向任何东西;这是一种守护者弹性模式。标记它只会浪费大家的时间。 - 用汇编编写的权限验证依然是权限验证。
if iszero(eq(sload(admin), caller())) { revert }对函数的保护效果与onlyOwner一样好。经过 Gas 优化的代码并不代表它是未受保护的代码。 对于模式匹配器来说,这些看起来都像漏洞,但实际上都不是。区分它们就是我的工作。
The discipline, stated plainly: Verify against reality, not against your assumptions. I nearly shipped a bug because I remembered a constant wrong. Read the actual value. Every time. Detectors surface; a human confirms. Heuristics are for recall — cast a wide net. Precision comes from a person who reads the code and asks “is this actually exploitable?” before it reaches the client. 简单来说,纪律就是:根据现实进行验证,而不是根据你的假设。我差点因为记错一个常量而提交了一个漏洞。每次都要读取实际数值。检测器负责浮现问题,人类负责确认。启发式算法用于召回——撒下大网。而精确度来自于阅读代码并询问“这真的可被利用吗?”的人,在报告到达客户之前进行把关。
A clean bill is a finding too. When I dug into that vault’s donation-attack surface and found the mitigation was there — reimplemented correctly, just not inherited from OZ — I wrote that down and said so. “It’s solid, and here’s why” is worth as much as a bug. It’s honest, and it’s what a team actually needs before they ship. 一份“无漏洞报告”也是一种发现。当我深入研究那个金库的捐赠攻击面,发现缓解措施已经存在——只是重新实现得很好,没有直接继承自 OZ——我记录了下来并指出了这一点。“它很稳健,原因如下”与发现一个漏洞同样有价值。这是诚实的,也是团队在发布前真正需要的。
The overflow that wasn’t taught me the same thing the clean vault did: the value isn’t in what you flag. It’s in what you can stand behind. 那个不存在的溢出漏洞教会了我与那个稳健的金库同样的事情:价值不在于你标记了什么,而在于你能为你的结论承担什么。
I do fast, honest Solidity security reviews — custom detectors plus manual verification, no false-positive noise. I only report what’s real, and I’ll tell you plainly when your code is solid. If that’s the kind of review you want before mainnet, my details are in my profile. 我提供快速、诚实的 Solidity 安全审计——自定义检测器加上人工验证,没有误报噪音。我只报告真实存在的问题,如果你的代码很稳健,我会直截了当地告诉你。如果你在主网上线前需要这种审计,我的联系方式在我的个人资料中。