关于编写安全的智能合约

关于编写安全的智能合约

译者注:本文作者是Matthew Di Ferrante, 是 ZK Labs 的创始人,也是一个知名智能合约开发者及审计人员。

经常被问及我的代码审计的流程是什么,很多时候我得到的感觉是,人们认为如果他们有一个足够详细的检查清单,就能使他们的代码安全。然而,安全不是一个检查清单,安全是一个过程,不仅在编写代码时,而且在项目和架构的设计时就得首先考虑,安全应该成为你心态的一部分。安全也不会在你部署代码时停止,随着你了解更多,看到新的机制被利用,你应该回忆你过去写的代码,并思考 这是否改变了我第一次写代码时的任何假设?。如果你开始对这种思考模式感到疲惫,并产生一种令人沮丧的偏执,这种偏执在你看代码时随时都会渗透到你的思想中,那么恭喜你,你已经走上了正确的道路。很多时候,我发现一个代码库的漏洞是通过阅读另一个代码库发现的,它以更完整的方式获取了一个边缘案例,并提醒我原来的代码库并没有这样做!这就是我的意识。正是这种的意识水平将帮助发现漏洞,无论是在你的代码中还是在别人的代码中。

因此,这篇介绍性文章,它是培养这种心态的一种方式,而不是认为只要你 清除了这里详述的一切,你就安全了。

开发和协作过程中的安全问题

安全问题产生的很大一部分原因是不言而喻的假设,而这大部分来自于缺乏沟通。无论是不准确或缺失的文档、过时的注释、误导性的代码,或仅仅是团队成员没有清晰说明代码的某个部分或处理问题--这一切都为漏洞的潜入提供了机会。每当你不确定某样东西是如何工作的,或者被误导认为你完全理解了它,你就会在一系列不正确或不完整的假设基础上前进和构建。结果是你刚写的东西的基础都有漏洞,而这些漏洞最终会导致问题出现。

举个例子:现在有多少人在写智能合约时知道ETH可以在不调用回退函数的情况下被发送到合约中,即使该函数是不可支付的(non-payable?)?它可以通过SELFDESTRUCT调用完成发送,余额被直接发送,而不是作为合约调用的一部分。

有多少智能合约在持有USDC时,在其内部逻辑中正确处理了这种情况:USDC管理员将他们的地址列入黑名单,并使所有的转账调用失败或抹去其余额?这些都是可能性,如果不加以说明,就会导致漏洞,由于与内部逻辑的不一致(余额),可能允许某人从合约中流出超过他们有权得到的资金,或者在其他情况下,最终意外地永远锁定资金。

译者注:USDC 是通过抵押美元发行的稳定币,背后有中心化组织控制,其有权限控制账号。

内部和外部假设

并不总是需要你创建的或使用的外部事物的属性来创造危险的假设。甚至在一个团队中,如果有人的代码看起来以一种方式工作,却奇技淫巧地做了其他事情,那么在该代码的基础上用这种破碎的假设进行构建,可能会导致在审计中没有发现的漏洞,因为如果暴露的属性足够细微,审计人员会根据他们自己已有认知(偏见)来审查代码,而不是基于已编写代码的。

这方面的一个例子,也是我在审计过程中经常指出的,就是用getSomeValue这样的被动动词来命名函数,而这个函数在获取一个值的同时巧妙地更新了一些状态。这很容易让人从函数名上就认为后者不会发生,因此最好是将其命名为updateYAndReturnX

代码越是具有误导性,从长远来看,对代码库的修改就越危险,因为代码的原作者会忘记原来所做的事情或者辞职离开了,导致团队了解到底发生了什么这样综合知识会越来越少。

编写文档经常被用作解决这个问题的方法,但如果文档半途而废,并且是以妥协的态度去编写文章,而不是理解为什么文档很重要,那就没有用了。如果不能仅仅通过文档来重新实现整个代码库达到一致的行为,那么当你在编写处理数千到数亿美元的关键系统时,文档都是不够的。API/集成文档也是如此,如果有什么东西你需要查看代码才能真正理解或避免陷阱,那么最终正的会有人落入陷阱。

依赖简单、安全的路径,整个工程都需要针对这一目标进行工作。

安全不仅仅是代码和技术细节的问题

即使100%地了解一个系统,也不能使你免于编写有漏洞的代码。深入了解你正在构建和使用的技术只是战役的一半,另一半是了解开发人员开发过程的心理以及为什么它自然地有利于创造漏洞。这后半部分通常不会被想到,但有许多小步骤可以采取,并有高额回报。

优化开发过程以促进沟通和安全的一个简单方法是,使代码库的每一次修改都需要同时对规范和文档进行修改和审查。如果原来的结构或布局在当前的复杂程度下变得不合适,那么规范本身应该被重构。

在文档中查找某个东西越困难,大脑就越不愿意集成到开发过程中。理想情况下,规范应该在代码被修改之前更新,这样审计人员就可以将 人类语言描述的意图与正在编写的代码进行比较,这样他们就不会意外地从代码中发现错误的意图。每当人们从代码中确定的意图感觉与规范中的内容不同时,这就是一个需要额外关注的信号。也许可以重构代码或架构来避免这种情况,也许这预示着在设计阶段根本没有考虑到某些机制或边缘情况,而这种差异会表现为一种不一致的感觉。

除了上述情况外,代码的意图总是与人有关,以及他们对世界的影响。在文档或规范中可以编写的一阶段和二阶段信息只有这么多。有时,更高的目标、长期的愿景或想象中的使用场景也不适合,在这些信息还没有或不能完全形成的时候,试图精确地抓住这些信息是没有用的。此时,可以(而且应该)非正式地与团队成员沟通, 每个人对他们的代码如何使用的看法越一致,目标和假设就越不可能发生分歧。如果每个人都对范围有一个心理上的共识,并真正理解他们所建立的平台,那么为确保安全不因假设错误而受到损害所需的认知开销就越少。

根据任务背后的意图不一样,代码可以有两种非常不同的 明显实现,而我们的目标是培养一种开发环境:使其有利于安全和正确的实现。

代码和方法上的提示

了解你的工具及其限制

在可能的情况下,总是使用最新的编译器,如果不是需要基于老编译器编写的代码开发的话。每个写 solidity 的人都应该至少略读一次文档。尤其是了解新版编译器的添加的功能。你知道solidity中的一个修改器可以有多个占位符吗?最新的编译器支持将函数指针作为参数传递给外部函数(但请限制你对该功能的使用)?如果这些事情让你感到惊讶,那么对于当前或未来的 Solidity 版本,你还有什么不知道的?你应该去了解一下。让你自己了解你所使用的工具的语义的最新情况。

你在解决什么问题,你在创造什么问题?

不要急于求成! 对你正在建造的东西要有100%的把握。不仅要考虑你的平台应该如何使用,还要考虑它不应该怎样使用--如何防范后者?思考其他能与你的平台交互的平台,利用它,或歪曲其动机。考虑你的平台是否改变别人可能做出的假设,以及为什么?

编写测试,检查正确和错误的结果

既要写确保功能正确的测试,也要写确保不应该发生的事情真的不能发生的测试。简单的例子:写一个测试,试图从一个你不持有余额的合约中提取余额,确保它恢复原状。错误测试对于确保你不会意外地将漏洞带回到代码库中是很重要的,维护它们也是提醒自己什么是不应该发生的,并在开发新的代码、功能或思考架构时保持头脑的 清晰

关注点分离,命名和理解你的代码

尽可能地通过代码结构、架构和继承来分离关注点,而不至于隐藏正确思考问题所需的细节。一个人在开发时需要记住的东西越少,由于忘记了重要的东西而引入错误的机会就越小。如果你在命名一个函数或合约,或描述它的作用时遇到困难,就把它分解,直到不再是问题。命名东西是很难的,因为它要求你把你正在做的事情外化,而要准确简单地外化它,你需要真正理解你正在建立的东西是什么。通常情况下,我们认为我们了解的比我们真正了解的多得多。认识到这一点并创建一个流程,避免将这种半懂不懂的东西引入到代码中,将极大地帮助安全、开发质量和速度,并减少时间的浪费。如果需要的话,可以根据你的具体领域来调整这个过程。

阅读其他人的代码!

最后,没有比阅读其他人的智能合约更好的投资回报率了--阅读比你差的人的代码,这样你可以从他们的错误中学习,来自比你好的人的代码,这样你可以看到你可能做错了什么,阅读尽可能多不同来源和领域的代码。你最终会开始感觉到大多数人在哪里犯错,什么是最难弄好的,这种意识会在你写自己的代码和处理棘手的实现细节时持续存在,你会知道你需要小心,不要高估你对这些代码的理解,或者错误地估计任务的复杂性和你弄好它的能力。


本翻译由 Duet Protocol 赞助支持。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO