本文讨论了智能合约中微小bug可能导致严重后果的问题,通过NASA火星探测器和ESA火箭的案例,说明了即使是简单的错误也可能造成巨大损失。文章还分析了一个智能合约审计中发现的实际案例,强调了在智能合约开发中进行严格测试和审计的重要性。
KOSTAS CHATZIKOKOLAKIS
正如大多数程序员会承认的那样,最令人恼火的 bug 往往是“小” bug。微小的逻辑错误是由一行代码中的几个错误字符引起的,编译正常且未被检测到,耐心地等待在最糟糕的时刻崩溃我们的程序。我们都写过这样的 bug,花费了无数的时间来调试它们,并且当我们最终发现我们因为几个错误的字符而失眠时,发出了最可怕的咒骂。
但是,为一个小 bug 失去一个晚上的睡眠并不是我们最担心的。至少如果有人为美国国家航空航天局 (NASA) 编写软件就不是这样,该机构的火星气候探测者号因软件 bug 而在火星大气层中烧毁而闻名。好吧,NASA 软件很复杂;这种灾难性的 bug 显然应该很复杂,凡人不可能理解,对吧?完全不是,导致损失 1.25 亿美元的火星气候探测者号的 bug 是一个微不足道但至关重要的缺少乘以 4.45。欧洲人也无法免受小 bug 的影响;ESA 价值 3.7 亿美元的 Ariane V 火箭在仅仅 39 秒内的损失是由一个简单的整数溢出错误引起的。
值得庆幸的是,在很长一段时间内,人们需要受雇于航天机构才能担心一个小 bug 会产生如此巨大的财务后果。直到智能合约的出现!现在,由小型团队在相对较短的时间内开发的,由几百行相对“简单”的代码组成的程序,直接负责保护各种价值数百万美元的资产。只需要一个未被发现的小 bug,我们得到的不是壮观的火箭爆炸,而是同样壮观的 加密黑客攻击 ,这使得火星气候探测者号看起来像是零钱。
因此,让我们看一个具有指导意义的这种小 bug 的例子。智能合约通常使用 Solidity 修饰器 (modifier) 来保护其函数,执行至关重要的安全检查。
modifier isOwner() {
// 确保在做任何事情之前,调用者是我们信任的 owner。
require(msg.sender == owner, "Caller is not owner");
_;
}
编写这样的检查很简单,无需成为 NASA 工程师即可完成。但最好仔细检查三遍,因为该行中最小的 bug 的后果是巨大的。
error CallerNotOwner(); // gas efficient and easy to recognize
// gas 高效且易于识别
modifier isOwner() {
// 我希望这是有效的代码,但事实并非如此。
require(msg.sender == owner, CallerNotOwner());
_;
}
你会说,没什么大不了的,require 只是检查和 revert 的组合;我们可以重写它并手动执行这两个步骤。
modifier isOwner() {
// 这工作正常
if(msg.sender != owner)
revert CallerNotOwner();
_;
}
任务完成,但你可能已经注意到一个小的细节。在上面的代码中,msg.sender == owner 被其 否定 替换:msg.sender != owner。这是因为 require 期望一个 应该 成立的条件,而它的 if/revert 替代方案期望一个 不应该 成立的条件。因此,一般来说,我们应该替换
require(some_complicated_expression, "my error");
通过
if(!some_complicated_expression)
revert MyError();
布尔表达式的这种否定正是我们的“小 bug”故事的开始。好吧,简单地添加一个“!”有多难?但这不完全是我们上面所做的,不是吗?没有哪个欣赏代码简洁和优雅的程序员会写
if(!(msg.sender == owner))
每个人都会将其简化为
if(!(msg.sender == owner))
将否定带入布尔表达式中。如果被否定的表达式更复杂怎么办?逻辑是计算机科学的基础,它为我们提供了简单的规则:
!(A && B) is equivalent to (!A || !B)
// 等价于
!(A || B) is equivalent to (!A && !B)
// 等价于
只需小心地遵循复杂布尔表达式中的规则,你就会没事的。说起来容易做起来难;我敢打赌,每个有几年经验的程序员都在职业生涯的某个时候错误地否定了一个布尔公式。
因此,这个确切的 bug 出现在 我们最近的一次审计中 并不奇怪。上面的 提交 (commit) 旨在用自定义错误替换字符串错误,并且这样做,更改了:
modifier onlyOwnerOrUpdater() {
require(
owner() == _msgSender() ||
(updater != address(0) && _msgSender() == address(this)),
"NetworkRegistry: !owner || !updater"
);
_;
}
至
modifier onlyOwnerOrUpdater() {
if (_msgSender() != owner() &&
(updater == address(0) && _msgSender() != address(this)))
revert NetworkRegistry__OnlyOwnerOrUpdater();
_;
}
你发现否定错误了吗?表达式的形式为 A || (B && C),因此其否定变为 !A && (!B || !C),B && C 中的 && 应该更改为 ||。因此,正确的检查应该是
if (_msgSender() != owner() &&
(updater == address(0) || _msgSender() != address(this)))
这两个错误的字符(&& 而不是 ||)完全改变了 修饰器 (modifier) 的逻辑;现在,一个未经授权的调用,其中 updater != address(0) 并且 _msgSender() != address(this)) 将 不会 像它应该的那样触发错误,这很容易导致此特定合约的资金完全损失。
当然,关键不是智能合约不可能得到保护:这个 bug 被审计发现了(发现它的机会很高),即使没有发现,我们也相信它仍然会在发布代码之前被发现,无论是通过手动检查还是自动化测试。
但它的存在表明,智能合约与所有程序一样,也无法免受小 bug 的影响。即使是最简单的更改也需要谨慎,并且应该通过内部和外部团队进行适当的测试和审计,以尽可能地减少灾难性小 bug 的机会。
- 原文链接: dedaub.com/blog/smart-co...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!