当心你的语言:我们对Vyper的首次审计
许多公司正在开发以太坊智能合约,但编写安全合约仍是一项艰巨的任务。开发者必须规避常见陷阱、编译器问题,并持续检查代码以应对新发现的风险。漏洞的常见来源之一是编程语言的早期状态。多数开发者使用Solidity,该语言因众多不安全行为而声名狼藉。如今,类似Python的Vyper语言旨在提供更安全的替代方案。随着社区对Vyper的兴趣增长,我们在最近与Computable的审计中不得不审查Vyper合约。
总体而言,Vyper是一种有前途的语言,它具有以下特点:
- 内置安全检查
- 提升代码可读性
- 简化代码审查
然而,Vyper的年轻也显现出来;我们的审查确认,这门年轻语言需要更多测试和工具。例如,我们在编译器中发现了一个错误,这表明缺乏深度测试。此外,Vyper尚未受益于Solidity所拥有的第三方工具集成,但我们正在努力:我们最近在crytic-compile中添加了Vyper支持,使Manticore和Echidna能够处理Vyper合约,Slither集成也在进行中。目前,您可以查看以下Vyper审计细节和建议。
优势
内置整数检查
Vyper带有内置的整数溢出检查,并在检测到溢出时回滚交易。由于整数溢出经常是漏洞的根源,默认的溢出保护无疑是向更安全合约迈出的重要一步。有了这种保护,您不再需要使用SafeMath等库。
但主要注意事项是更高的燃气成本。例如,编译器会为以下代码添加两个SLOAD:
图1:整数溢出检查示例
图2:图1示例的evm_cfg_builder结果
尽管如此,默认溢出保护仍然是最佳策略。未来,Vyper可以通过优化(例如从上述示例中移除两个SLOAD)或为有特定需求的开发者添加不安全类型来降低燃气成本。
限制不安全功能
与Solidity相比,Vyper有许多限制,包括:
- 无继承
- 无递归代码
- 无无限长度循环
- 无动态大小数组
- 无汇编代码
- 无法从另一个文件导入逻辑
- 无法从一个合约创建另一个合约
尽管这些限制可能显得过于严格,但大多数合约仍可在遵循这些规则的情况下实现。
Solidity允许多重继承,开发者经常过度使用此功能。我们见过许多代码库具有过于复杂的继承图,这使得代码审查比应有的困难得多。实际上,多重继承使合约难以跟踪,我们不得不构建专用打印机在Slither中输出继承图。防止多重继承将迫使开发者创建更好的设计。
Solidity还允许汇编代码,这通常用于补偿编译器优化的不足。当无法在开发者级别编写这些优化时,Vyper编译器团队编写良好编译器优化的压力更大。这不是坏事——优化应依赖编译器,而非开发者。
总体而言,由于Vyper的许多语言限制,Slither三分之一的检测器在使用Vyper时不再需要。可以编写Vyper特定检测器,但语言的简洁性使其在设计上比Solidity更安全。
不足之处
Vyper未经充分测试或审查
因此,编译器可能存在错误,语言的语法和语义可能会更改。Vyper用户必须小心,密切关注其发展,并审查生成的EVM字节码。
例如,在0.1.0b12之前,公共函数可从合约本身调用,由于Vyper处理msg.sender和msg.value的方式,这造成了安全风险。自0.1.0b12起,所有公共函数等同于Solidity中的外部函数,消除了此漏洞的风险。
我们发现的编译器错误表明编译器需要更多测试(详见下文)。在Vyper中发现先前solc错误并不令人惊讶。例如,以下错误最近已修复或仍然存在:
- 一元操作缺乏溢出检查
- 事件缺乏类型检查
- 返回小数组时零填充不正确
某些限制繁琐
虽然Vyper的许多限制是向更安全代码迈出的好步骤,但有些可能造成问题。
例如,完全缺乏继承使代码测试更加困难。创建模拟合约或添加属性以使用Echidna测试需要复制粘贴代码——这是一个容易出错的过程。尽管开发者经常滥用多重继承,但允许简单继承以方便测试并无害处。
与缺乏继承类似,缺乏合约创建也不方便——它增加了模拟合约、单元测试和自动化测试的复杂性。
最后,每个合约必须写在单独的文件中,并且导入功能部分支持。如果合约A调用合约B,A需要知道B的接口。然后开发者有责任复制粘贴最新接口版本。如果B已更新但A中的接口未更新,A将有错误且容易出错处理合约依赖关系。为防止这些类型的漏洞,我们构建了slither-dependencies,该工具将检查代码库中的正确接口。
我们的解决方案
编译器错误:函数碰撞
Vyper遵循Solidity使用的函数分发器标准:要调用函数,函数签名的keccack哈希的前四个字节将用作标识符。所谓分发器负责将标识符与要执行的正确代码匹配。在图3中,分发器检查两个不同的函数id:
- 0x0e8927fbc(推入0x94):increase()
- 0x61bc221a(推入0xcb):counter()
图3:图1示例的分发器
此策略有一个缺点:四个字节很小,可能发生碰撞。例如,gsf()和tgeo()都将导致id为0x67e43e43。图4显示了使用vyper 0.1.0b10生成的分发器:
图4:函数id碰撞
因此,调用tgeo()将执行gsf()代码,而tgeo()将永远无法执行。此问题为后门合约创造了完美条件。我们向Vyper团队报告了此错误,并于7月修复。他们的初始修复未考虑与回退函数碰撞的极端情况,但现在也已正确修复。
最后,我们在Slither中实现了一个检测器来捕获此错误。如果您担心与Vyper合约交互,请使用Slither。
Crytic工具集成
自crytic-compile 0.1.3起,Vyper现在得到我们大多数工具(包括Manticore、Echidna和evm-cfg-builder)的本机支持。
Manticore Manticore是一个符号执行框架,可让您证明代码中的断言。它在EVM级别工作,这对于避免潜在编译器错误是必要的。例如,以下代币有一个错误,将为请求少于10个代币的任何人提供免费代币:
图5:Vyper有错误代币
以下Manticore脚本将检测此问题:
图6:使用Vyper的Manticore示例
脚本将生成显示导致错误的输入的交易:
|
|
开发者随后可以将脚本集成到CI中以检测错误,并在修复后证明其不存在。查看Computable Manticore脚本以获取更多示例。
Echidna Echidna是一个属性测试模糊器:它尝试不同的输入组合,直到成功破坏给定属性。与Manticore一样,它在EVM级别工作。在以下示例中,Echidna尝试调用组合,直到echidna_test函数返回false:
图7:Echidna示例
在图7示例上运行Echidna时,结果是:
图8:在Vyper代码上运行Echidna
与Manticore类似,Echidna可以与CI集成以在开发过程中检测错误。关注crytic.io以获取使用Echidna的简便解决方案。
Slither 我们正在努力在静态分析器中支持Vyper。Slither已经能够:
- 检测代码是否易受我们发现的碰撞id编译器错误影响
- 检测是否存在不正确的外部合约定义(通过slither-dependencies)
Vyper支持完成后,Vyper合约将受益于我们的中间表示(SlithIR),并访问我们框架中已有的所有漏洞检测器和代码分析。
结论
Vyper是向更好智能合约语言迈出的良好一步。我们喜欢其简洁性和对安全的关注。然而,该语言过于年轻,无法推荐用于生产。如果您想使用Vyper,我们强烈建议使用Manticore和Echidna检查EVM代码,并关注Slither的发展。
已经喜欢Vyper并想保护您的代码?联系我们!