警惕语言特性:我们的首次Vyper审计
许多公司正在开发以太坊智能合约,但编写安全合约仍是艰巨任务。开发者仍需避免常见陷阱、编译器问题,并持续检查代码以应对新发现风险。漏洞的常见来源在于编程语言的早期状态。多数开发者使用Solidity,该语言因众多不安全行为而声名狼藉。如今,类似Python的Vyper语言旨在提供更安全的替代方案。随着社区对Vyper兴趣增长,我们近期与Computable合作审计了Vyper合约。
总体而言,Vyper是一种前景广阔的语言,其特点包括:
- 内置安全检查
- 提升代码可读性
- 简化代码审查
然而Vyper的稚嫩已然显现:我们的审查确认这门年轻语言需要更多测试和工具支持。例如我们在编译器中发现的错误表明缺乏深度测试。此外Vyper尚未像Solidity那样受益于第三方工具集成,但我们正在推进相关工作:近期为crytic-compile添加了Vyper支持,使Manticore和Echidna能够处理Vyper合约,Slither集成也在进行中。
优势特性
内置整数检查
Vyper配备内置整数溢出检查,检测到溢出时会自动回滚交易。由于整数溢出常是漏洞根源,默认启用溢出保护无疑是迈向安全合约的重要一步。有了这种保护,就不再需要SafeMath等库。
主要注意事项是燃气成本增加。例如编译器会为以下代码添加两个SLOAD操作: (图1:整数溢出检查示例) (图2:图1示例的evm_cfg_builder结果)
尽管如此,默认溢出保护仍是最佳策略。未来Vyper可通过优化降低燃气成本(例如移除上述示例中的两个SLOAD),或为有特殊需求的开发者添加不安全类型。
限制不安全功能
相比Solidity,Vyper存在诸多限制:
- 无继承机制
- 无递归代码
- 无无限长度循环
- 无动态大小数组
- 无汇编代码
- 无法从其他文件导入逻辑
- 无法从另一个合约创建合约
虽然这些限制看似严苛,但大多数合约仍可在遵守这些规则的前提下实现。
Solidity允许多重继承,开发者经常过度使用此功能。我们见过许多代码库具有过度复杂的继承图谱,使代码审查变得异常困难。实际上,多重继承使合约难以追踪,我们不得不专门在Slither中构建继承图谱输出器。禁止多重继承将迫使开发者创建更合理的设计。
Solidity还允许汇编代码,这常被用于补偿编译器优化的不足。当开发者无法实现这些优化时,Vyper编译器团队编写优秀编译器优化的压力更大。这并非坏事——优化应依赖编译器而非开发者。
总体而言,由于Vyper的语言限制,Slither三分之一的检测器在Vyper中不再需要。虽然可以编写Vyper专用检测器,但语言的简洁性使其在设计上比Solidity更安全。
现存问题
测试与审查不足
“因此编译器可能存在错误,语言语法和语义可能更改。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使用的函数分发器标准:调用函数时,函数签名keccak哈希的前四位字节将用作标识符。所谓分发器负责将标识符与要执行的正确代码匹配。在图3中,分发器检查两个不同函数ID:
- 0x0e8927fbc(推入0x94):increase()
- 0x61bc221a(推入0xcb):counter()
(图3:图1示例的分发器)
此策略存在缺陷:四位字节太小,可能发生冲突。例如gsf()和tgeo()都将导致0x67e43e43的ID。图4显示vyper 0.1.0b10生成的分发器: (图4:函数ID冲突)
因此调用tgeo()将执行gsf()代码,tgeo()永远无法执行。此问题为后门合约创造了完美条件。我们向Vyper团队报告了此错误并于7月修复。他们的初始修复未考虑与fallback函数冲突的特殊情况,但现已完全修复。
最后我们在Slither中实现了检测此错误的检测器。如果您担心与Vyper合约交互,请使用Slither。
Crytic工具集成
自crytic-compile 0.1.3起,我们的多数工具(包括Manticore、Echidna和evm-cfg-builder)已原生支持Vyper。
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并想确保代码安全?请联系我们!