引言
以太坊智能合约的安全问题源于多个方面。智能合约运行在一个无需许可的网络中,任何人都可以加入,并且它们可能持有巨大的货币价值。此外,代码是公开的,且由于网络的不可变性,修复错误变得异常困难。
作为市值最大的图灵完备区块链,与以太坊网络的交互成本高昂,这源于其最大吞吐量限制和高昂的以太币价格。网络常常拥堵,导致 Gas 费用居高不下。由于执行环境对开发者而言更为陌生,代码由全球匿名节点网络运行,且以太坊技术栈处于快速迭代中,新的安全问题不断出现,因此遵循最佳实践变得至关重要。
在区块链特有的漏洞中,绝大多数都可以通过应用最佳实践来避免。研究表明,保证智能合约的安全性十分困难,其中一个主要原因正是缺乏明确的最佳实践指南。本文将结合先前研究、社区知识及实践发现,旨在创建一份易于遵循且包含最重要信息的最佳实践清单。
关键概念解析
Gas 费用机制
由于以太坊虚拟机(EVM)是图灵完备的,它可以执行包含不确定性的代码。为了防止恶意用户利用无限循环或编程错误导致整个网络崩溃,以太坊引入了网络使用费用机制。
- Gas: 衡量计算资源消耗的单位。
- Gas Limit: 用户设置代码运行所允许的最大步骤数。若代码在执行完毕前耗尽 Gas,交易将被回滚,但费用仍需支付。
- Gas Price: 交易中每单位 Gas 的价格,通常以 Gwei 计价。
- 基础费用(Base Fee)和优先费用(Priority Fee): 基础费用是交易被纳入区块所必须支付的费用,由前一个区块计算得出,每个区块最多增长12.5%。基础费用会被销毁,只有优先费用支付给验证者。优先费用越高,交易被优先打包的可能性越大。
每个区块都有网络能处理的最大吞吐量。当许多参与者同时想要交互时,网络变得拥堵,基础费用随之上涨,用户通过提高费用来竞相进入区块。
Solidity 优化器
优化器程序通过减少代码大小并提高其效率来降低 Gas 消耗。Solidity 编译器内置了优化器,包含“旧”优化器(基于操作码)和“新”优化器(基于 Yul 中间语言)。它们通过简化规则、移除死代码、合并相同代码集等方式进行优化,从而降低函数调用和合约部署的成本。
区块链安全
以太坊的安全对于网络的安全使用至关重要。其无需许可和不可变的特性允许恶意用户随意攻击,且通常无人能更改区块链上的代码。尽管在极端情况下(如 The DAO 攻击)有过干预先例,但安全责任主要落在开发者和用户身上。
不可变性与代理合约
部署在以太坊上的智能合约代码默认是不可变的。这使得修复已部署代码中的 bug 变得不可能。为了绕过此限制,开发者可以部署多个包含代码的智能合约,并使用一个代理合约来链接这些合约的实现。代理合约的可修改存储中存储着指向其他合约的地址。
升级模式主要分为零售变更(对组件进行小修改)和批发变更(修改整个合约)。常见的实现方式包括基于 DELEGATECALL 或 CALL 的数据分离模式,它们通常涉及一个代理合约、一个存储合约和一个逻辑合约。
核心开发策略
通用开发理念
- 追求简洁: 优先选择简单的合约而非复杂的合约。复杂性是安全性的敌人。简洁的代码更易于理解程序流程和代码意图,让 bug 无处藏身。这并非要避免创建功能丰富的合约,而是强调应从简单的起点开始,逐步构建高质量的复杂功能。
- 预期失败: 为代码可能失败做好准备。没有任何代码能保证没有 bug,在以太坊上,停止、暂停或移除故障服务通常比传统软件更困难。为此,可以考虑引入升级机制、暂停关键功能的能力或使用速率限制来减少最大使用量或提现额。
- 策略适配: 没有放之四海而皆准的“银弹”技术。开发策略首先需要从传统软件工程适配到区块链开发,其次,不同的智能合约之间也需进行调整。应根据合约的具体要求选择不同的解决方案。
安全最佳实践
安全应被视为最高优先级,其重要性远超其他方面。
- 代码复用与库使用: 复用经过充分测试和广泛使用的库代码,通常比从头开始创建一切更安全。OpenZeppelin 库就是一个 prominent 的例子。但需注意,代码复用可能比传统系统带来更高的风险,因此复用的合约应经过安全审计。
- 合约升级的权衡: 对于更复杂、生命周期更长的智能合约,建议使用代理模式或其他可升级模式来修复关键缺陷。虽然这引入了额外的复杂性和潜在的安全漏洞,但其能够修复 bug 的好处对于大型项目而言是值得的。
- 检查-效果-交互模式(Check-Effect-Interact): 作为一种防御性安全机制,Solidity 代码的一般顺序应是:先检查条件,随后更新状态,最后才进行外部调用。这有助于防止重入攻击。
- 拉取优于推送(Pull Over Push): 对于外部调用或 ETH 转账,实现一个由接收方主动发起的拉取机制,而非由合约主动推送。这可以避免因意外回滚导致的拒绝服务攻击。
- 代币标准: 根据用例选择合适的代币标准(如 ERC-20, ERC-721, ERC-1155)。使用广泛采用的标准可以基于开源社区的经验进行构建。
- 代码可读性: 清晰简单的代码更易于审查和审计。应遵循风格和命名约定,并记录代码。警惕那些以牺牲可读性为代价的 Gas 优化技巧。
- 测试与保持更新: 实现高测试覆盖率,并在开发的多个阶段利用测试网络进行测试。始终保持 Solidity 的当前稳定版本以及所有开发工具和库的最新状态,并及时关注新发现的安全漏洞。
- 安全的以太坊交互: 妥善管理私钥,对诈骗行为保持警惕意识。考虑到以太坊的去中心化性质,在发生密钥丢失时没有中央机构可以提供帮助。
Gas 优化深入解析
Gas 优化策略分为通用型和高级型。通用技巧通常成本低、效益高,只需很少努力即可带来巨大收益。高级技巧则需要更深入的 Solidity 知识,收益相对较小,且可能增加复杂性。
通用 Gas 优化策略
- 启用 Solidity 优化器: 对于简单的 ERC-721 和 ERC-1155 合约,优化器可减少超过 40% 的部署 Gas 成本。建议始终启用优化器,并根据合约是被频繁执行还是一次性部署来调整
runs参数(默认值为200),以优化部署大小或执行成本。 - 选择部署时机与利用网络拥堵状态: 部署或与链交互的成本受网络拥堵状态影响巨大。在 UTC 时间的夜间通常网络更空闲。对于非紧急交易,可以设置一个较低的
maxFeePerGas,使其在网络拥堵缓解时再被处理,从而显著降低成本。但需注意,费用不应设置过低以免交易永远无法执行,且此方法不适用于时间紧迫的交易。 - 谨慎使用存储: 以太坊全节点需要存储所有网络数据,因此存储操作非常昂贵。应考虑使用 IPFS 等外部存储解决方案来处理大型数据。
- 更新 Solidity 版本: 新版本的 Solidity 不仅提升安全性,也有助于降低 Gas 成本。例如,自 Solidiy 0.8.0 起内置的溢出检查移除了对 SafeMath 库的依赖;0.8.4 引入的自定义错误(Custom Errors)降低了运行时和部署的 Gas 成本。
高级 Gas 优化技巧
这些技巧更适用于库或其他频繁使用的智能合约。
- 最小代理合约(Minimal Proxy Contract): 使用 ERC-1167 标准的最小代理合约可以低成本地克隆合约功能,仅实现部分逻辑并委托给另一个持有主逻辑的合约,从而降低部署成本。
- 缓存存储值: 对于需要多次读取的存储变量,应将其先复制到内存中,随后从内存中读取,以减少昂贵的存储读取操作。
- 使用位图(Bitmap): 使用位图可以紧凑且高效地保存数据(每个条目仅占1位而非8位),并且从非零值改写为非零值的存储成本远低于从零改写为非零。
- 变量打包: EVM 以 32 字节的槽为单位操作。如果将较小的状态变量在合约中彼此相邻声明,Solidity 编译器会将它们打包到同一个存储槽中,从而节省存储空间和 Gas。
- 使用 UINT256: 如果变量没有被打包,使用
uint256类型通常更省 Gas,因为其他大小的变量每次使用时可能需要转换为uint256。
工具使用与审计
使用辅助工具时需牢记两点:首先,使用安全工具并不能保证绝对安全;其次,使用 Gas 优化器也可能带来意外问题甚至引入漏洞。因此,必须权衡可能的副作用。
研究表明,不同工具之间的缺陷识别一致性较低,因此建议组合使用多种工具(如 Mythril 和 Slither)以实现最佳覆盖。务必选择经过充分测试和维护的工具。
常见问题(FAQ)
1. 为什么以太坊智能合约的安全性如此重要?
智能合约通常管理着具有实际价值的资产,且一旦部署便难以更改。此外,网络无需许可的特性意味着任何恶意行为者都可以随时尝试攻击漏洞,导致资金损失的风险极高。
2. 如何平衡 Gas 优化与代码可读性?
安全性和可读性应优先于微小的 Gas 节省。对于一次性的或简单的合约,可读性更重要。对于那些将被频繁调用或作为库广泛使用的合约,可以考虑在保证代码清晰的前提下实施更高级的优化,并对优化进行充分注释。
3. 代理合约升级模式的主要风险是什么?
代理模式引入了额外的复杂性,代理合约和逻辑合约访问相同的状态变量存在覆盖风险。如果实现不当,升级机制本身可能成为新的攻击向量。因此,必须严格遵循经过审计的库(如 OpenZeppelin)提供的实现标准。
4. 启用 Solidity 优化器是否有风险?
早期版本的优化器确实存在一些 bug,但开发团队自2020年底以来已建议始终启用优化器。只要使用稳定版本而非实验性功能,其带来的巨大 Gas 节省效益远超过潜在风险。
5. 对于其他 EVM 兼容链(如 Polygon, BSC),这些策略同样适用吗?
是的,通用最佳实践和安全策略在很大程度上是适用的。但由于这些链的交易费用通常低得多,Gas 优化的紧迫性和收益相对较小,开发重点可以更侧重于功能实现和安全性。
6. 开发者如何应对以太坊快速发展的技术栈?
保持学习至关重要。积极关注 Solidity 官方博客、以太坊基金会动态、知名审计公司的报告以及核心社区讨论,及时了解新工具、新版本的最佳实践和新出现的漏洞,并适时更新项目依赖和代码模式。
结语
以太坊智能合约开发是一个要求高度责任感和持续学习的领域。成功的关键在于将安全性置于绝对优先地位,优先采用经过实践检验的模式和库,并对 Gas 成本保持敏锐的意识。记住,没有一成不变的规则,最有效的策略总是根据项目的具体规模、复杂性和预期生命周期来量身定制。通过遵循结构化的最佳实践,开发者可以更有信心地构建安全、高效且可持续的区块链应用。