智能合约安全性
页面最后更新: 2026年1月15日
智能合约极为灵活,能够控制大量的价值和数据,并在区块链上运行基于代码的不可改变逻辑。 因而,一个由去信任的去中心化应用程序构成的生态系统应运而生且充满活力,它具备了许多传统系统所没有的优势。 同时,这也给攻击者提供了利用智能合约中的漏洞来获利的机会。
公共区块链(比如以太坊)使智能合约的安全性问题变的更加复杂。 已部署的合约代码_通常_无法更改以修补安全漏洞,而由于其不可变性,从智能合约中盗取的资产极难追踪且基本上无法追回。
虽然统计数据有所差异,但据估计,由于智能合约的安全缺陷而被盗窃或丢失的资产总额肯定超过了 10 亿美元。 其中包括一些备受瞩目的事件,例如 DAO 黑客攻击opens in a new tab(360 万 ETH 被盗,按当前价格计算价值超过 10 亿美元)、Parity 多签钱包遭黑客攻击opens in a new tab(黑客窃取了 3000 万美元),以及 Parity 钱包冻结问题opens in a new tab(价值超过 3 亿美元的 ETH 被永久锁定)。
上述几个事件迫使开发者必须付诸努力,构建安全、稳健、恢复力强的智能合约。 智能合约安全性是每个开发者都需要学习和研究的严肃问题。 本指南将介绍针对以太坊开发者的安全性注意事项,并研究增强智能合约安全性的资源。
前提条件
在着手处理安全问题之前,请确保您熟悉智能合约开发的基础知识。
构建安全的以太坊智能合约指南
1. 设计适当的访问控制
在智能合约中,标记为 public 或 external 的函数可由任何外部持有账户 (EOA) 或合约账户调用。 如果你希望他人与你的合约交互,就必须为函数指定公共可见性。 然而,标记为 private 的函数只能被智能合约内部的函数调用,外部帐户无法调用。 为每个网络用户提供合约函数的访问权限会造成问题,尤其是当这种访问意味着任何人都能执行敏感操作(比如铸币)的情况。
为了防止未经授权使用智能合约函数,有必要实现安全访问控制。 访问控制机制将使用智能合约中某些特定函数的能力限定给经过核准的实体,例如负责管理合约的帐户。 所有权模式和基于角色的控制是在智能合约中实现访问控制的两种有用模式:
所有权模式
在所有权模式中,在合约创建过程中将地址设置为合约的“所有者”。 受保护的函数被分配了一个 OnlyOwner 修饰符,这能确保合约在执行函数前验证调用地址的身份。 从合约所有者以外的其他地址调用受保护的函数,始终会被回滚,阻止不必要的访问。
基于角色的访问控制
在智能合约中将单个地址注册为 Owner 会引入中心化风险,并构成单点故障。 如果所有者的帐户密钥已泄露,攻击者就可以攻击其拥有的合约。 这就是采用基于角色的访问控制模式及多个管理帐户可能是更好方案的原因。
在基于角色的访问控制中,对敏感函数的访问分布在一组受信任的参与者之间。 例如,一个帐户可能负责铸造代币,而另一个帐户进行升级或暂停合约。 以这种方式分散访问控制,消除了单点故障并减少了对用户的信任假设。
使用多重签名钱包
实现安全访问控制的另一种方法是使用多签账户来管理合约。 与常规外部帐户不同,多重签名帐户由多个实体拥有,需要最低数量的帐户签名(比如 5 个中的 3 个)才能执行交易。
使用多重签名进行访问控制增加了额外一层安全性保障,因为需要多方同意才能对目标合约执行操作。 如果有必要使用所有权模式,这种方法尤其有用,因为攻击者或内部作恶者操控敏感的合约函数以达到恶毒目的会更加困难。
2. 使用 require()、assert() 和 revert() 语句来保护合约操作
如上所述,一旦智能合约部署到区块链上,任何人都可以调用其中的公共函数。 由于无法事先知道外部帐户将如何与合约交互,因此最好在部署之前实施内部安全措施以防出现有问题的操作。 您可以通过使用 require()、assert() 和 revert() 语句在智能合约中强制执行正确的行为,以便在执行未能满足某些要求时触发异常并回滚状态变更。
require():require 在函数开头定义,确保在执行被调用的函数前满足预定义的条件。 require 语句可用于验证用户输入、检查状态变量或在函数继续执行前验证调用账户的身份。
assert():assert() 用于检测内部错误和检查代码中是否违反“不变量”。 不变量是关于合约状态的逻辑断言,对于函数的所有执行都应该为真。 举个例子,代币合约的最大总供应量或余额就是一个不变量。 使用 assert() 可以确保您的合约永远不会进入易受攻击的状态,如果进入了,对状态变量的所有更改都会被回滚。
revert():revert() 可用于 if-else 语句,在不满足所需条件时触发异常。 下面的示例合约使用 revert() 来保护函数的执行:
1pragma solidity ^0.8.4;23contract VendingMachine {4 address owner;5 error Unauthorized();6 function buy(uint amount) public payable {7 if (amount > msg.value / 2 ether)8 revert("提供的以太币不足。");9 // 执行购买操作。10 }11 function withdraw() public {12 if (msg.sender != owner)13 revert Unauthorized();1415 payable(msg.sender).transfer(address(this).balance);16 }17}显示全部3. 测试智能合约并验证代码正确性
在以太坊虚拟机中运行的代码具有不可变性,这意味着智能合约在开发阶段需要更高水平的质量评估。 对合约进行大量测试并观察是否存在任何意外结果,将显著增强合约的安全性并为用户提供长远保护。
常用办法编写小单元测试,这些测试使用预计合约从用户处接收的模拟数据。 单元测试有助于测试特定函数的功能,并确保智能合约按预期工作。
遗憾的是,单独使用单元测试对提高智能合约的安全性效果甚微。 单元测试也许可以证明函数对于模拟数据正确执行,但单元测试的有效性受限于编写的测试。 这就意味着很难检测到威胁智能合约安全性的边缘情况和漏洞。
更好的方法是将单元测试与使用静态和动态分析执行的基于属性的测试相结合。 静态分析依赖于低级别表示(例如控制流图opens in a new tab和抽象语法树opens in a new tab) 来分析可达的程序状态和执行路径。 同时,动态分析技术(例如智能合约模糊测试opens in a new tab)通过使用随机输入值执行合约代码来检测违反安全属性的操作。
形式化验证是验证智能合约安全属性的另一种技术。 与常规测试不同,形式化验证能够确证智能合约中没有错误。 这是通过制定细致描述安全属性的形式化规范并证明智能合约的形式化模型符合这一规范来实现的。
4. 请求对您的代码进行独立审查
在测试智能合约后,最好请其他人检查源代码是否存在安全问题。 虽然测试无法发现智能合约中的所有缺陷,但进行独立审核能增加发现漏洞的可能性。
审计
进行独立代码审核的方式之一是委托执行智能合约审计。 审计员是确保智能合约安全、没有质量缺陷和设计错误的关键所在。
尽管如此,你也不应将审计看作终极方案。 智能合约审计无法发现所有漏洞并且主要是为了额外增加一轮审核,这有助于检测到开发者在最初的开发和测试中遗漏的问题。 你还应遵循与审计员合作的最佳做法(例如正确记录代码并添加行内注释),让智能合约审计发挥最大作用。
- 智能合约审计技巧和诀窍opens in a new tab - @tinchoabbate
- 充分利用您的审计opens in a new tab - Inference
漏洞赏金
执行外部代码审查的另一种方法是设立漏洞奖励计划。 漏洞奖励是一种经济奖励,提供给发现应用程序中漏洞的个人(通常是白帽黑客)。
应用得当,漏洞奖励可以激励黑客群体中的成员检查你的代码是否存在重大缺陷。 一个真实的例子是“无限金钱漏洞”,该漏洞可能让攻击者在 Optimismopens in a new tab(一个在以太坊上运行的第 2 层协议)上创建无限量的以太币。 幸运的是,一位白帽黑客发现了这个漏洞opens in a new tab并通知了团队,在此过程中获得了一大笔奖金opens in a new tab。
一种实用策略是按有风险资金数额的比例设置漏洞奖励计划的报酬金额。 这种方法被称为“分级漏洞赏金opens in a new tab”,它为个人提供经济激励,鼓励他们负责任地披露漏洞,而不是利用漏洞。
5. 在智能合约开发过程中遵循最佳实践
即使审计和漏洞奖励存在,你也有责任编写高质量的代码。 遵循正确的设计和开发流程是良好的智能合约安全性的开端:
-
将所有代码存放在一个版本控制系统中,例如 git
-
所有代码修改通过拉取请求进行
-
确保拉取请求至少有一位独立审核者 — 如果只有你一人完成项目,考虑和其他开发者相互进行代码审核
-
使用开发环境来测试、编译、部署智能合约
-
使用基础代码分析工具(例如 Cyfrin Aderynopens in a new tab、Mythril 和 Slither)运行您的代码。 理想情况下,应在合并每个拉取请求前进行这一操作,并比较输出中的不同之处
-
确保代码在编译时没有错误,并且 Solidity 编译器没有发出警告
-
(使用 NatSpecopens in a new tab)妥善地为代码编写文档,并用通俗易懂的语言描述合约架构的细节。 这将使其他人更容易审计和审核你的代码。
6. 实施稳健的灾难恢复计划
设计安全的访问控制、使用函数修改器以及其他建议能够提高智能合约的安全性,但这些并不能排除恶意利用的可能性。 构建安全的智能合约需要“做好失败准备”,并制定好应变计划有效地应对攻击。 适当的灾难恢复计划应包括以下部分或全部内容:
合约升级
虽然以太坊智能合约默认是不可变的,但通过使用升级模式可以实现一定程度的可变性。 如果重大缺陷导致合约不可用并且部署新逻辑是最可行的选择,有必要升级合约。
合约升级机制的原理有所不同,但“代理模式”是智能合约升级最常见的方法之一。 代理模式opens in a new tab将应用程序的状态和逻辑拆分到_两个_合约中。 第一个合约(称为“代理合约”)存储状态变量(如用户余额),而第二个合约(称为"逻辑合约")存放执行合约函数的代码。
账户与代理合约交互,代理合约使用 delegatecall()opens in a new tab 低层级调用将所有函数调用分派到逻辑合约。 与常规消息调用不同,delegatecall() 确保在逻辑合约地址运行的代码在调用合约的上下文中执行。 这意味着逻辑合约将始终写入代理的存储(而不是它自己的存储),并且 msg.sender 和 msg.value 的原始值会被保留。
将调用委托给逻辑合约需要将其地址存储在代理合约的存储空间。 因此,升级合约的逻辑就相当于部署另一个逻辑合约并在代理合约中存储新的地址。 由于对代理合约的后续调用会自动传送到新的逻辑合约,因此你“升级”了合约,但实际上并未修改代码。
紧急停止
如上所述,大量审计和测试不可能发现智能合约中的所有漏洞。 无法修补在部署后出现的代码漏洞,因为你无法更改运行在智能合约中的代码。 而且,升级机制(如代理模式)可能需要时间来实现(它们往往需要多方批准),这只会给攻击者更多的时间来造成更大的破坏。
这种情况下,核心方案是实施一种“紧急停止”功能,阻止对合约中有漏洞的函数的调用。 紧急停止通常由以下几部分组成:
-
表明智能合约是否处在停止状态的全局布尔变量。 设置合约时,此变量被设置为
false,但一旦合约停止,它将恢复为true。 -
执行过程中引用该布尔变量的函数。 此类函数在智能合约没有停止时可以访问,而当紧急停止功能触发后则无法访问。
-
有权访问紧急停止功能的实体,可将布尔变量设置为
true。 为防止恶意行为,对此功能的调用可以限制给一个可信地址(如合约所有者)。
一旦合约操作触发紧急停止,某些函数将无法调用。 这是通过把一些函数包装在引用该全局变量的修改器中实现的。 下文是描述在合约中实现此模式的一个示例opens in a new tab:
1// 此代码未经专业审计,不保证其安全性或正确性。使用风险自负。23contract EmergencyStop {45 bool isStopped = false;67 modifier stoppedInEmergency {8 require(!isStopped);9 _;10 }1112 modifier onlyWhenStopped {13 require(isStopped);14 _;15 }1617 modifier onlyAuthorized {18 // 在此处检查 msg.sender 的授权19 _;20 }2122 function stopContract() public onlyAuthorized {23 isStopped = true;24 }2526 function resumeContract() public onlyAuthorized {27 isStopped = false;28 }2930 function deposit() public payable stoppedInEmergency {31 // 此处是存款逻辑32 }3334 function emergencyWithdraw() public onlyWhenStopped {35 // 此处是紧急取款逻辑36 }37}显示全部以上示例展示了紧急停止的基本特点:
-
isStopped是一个布尔值,开始时为false,当合约进入紧急模式时为true。 -
函数修饰符
onlyWhenStopped和stoppedInEmergency会检查isStopped变量。stoppedInEmergency用于控制在合约易受攻击时应无法访问的函数(例如,deposit())。 对这些函数的调用将仅仅进行回滚而已。
onlyWhenStopped 用于在紧急情况下应可调用的函数(例如 emergencyWithdraw())。 此类函数可以帮助解决问题,因此它们不在“受限制函数”之列。
紧急停止功能的应用,为处理智能合约中的严重漏洞提供了一种有效的权宜之计。 然而,这也意味着用户更需要相信开发者不会为自身利益激活这一功能。 为此,将紧急停止的控制权去中心化,使其受到链上投票机制、时间锁的约束或者需要来自多重签名钱包的批准,都是潜在的解决方案。
事件监控
事件opens in a new tab允许您跟踪对智能合约函数的调用并监控状态变量的变更。 最理想的做法是将智能合约编写为能够在某一方采取对安全至关重要的操作(如提取资金)时发出一个事件。
记录事件并进行链下监测,可以深入了解合同的运作情况,有助于更快地发现恶意行为。 这意味着你的团队可以更快地应对黑客攻击并采取行动减轻对用户的影响,如暂停函数或进行升级。
你也可以选择一种现成的监测工具,只要有人与你的合约交互,就会自动转发警报。 这些工具将允许你根据不同的触发器创建自定义警报,如交易量、函数调用的频率或相关具体函数。 例如,你可以编写一个警报,当单笔交易的提款金额超过特定阈值时触发。
7. 设计安全的治理系统
你可能想要通过将核心智能合约的控制权转交给社区成员来去中心化你的应用。 在这种情况下,智能合约系统将包括一个治理模块 — 一种允许社区成员通过链上治理系统批准管理行为的机制。 例如,将代理合约升级为新实现的提案可能由代币持有人投票。
去中心化治理可能是有益的,特别是因为它符合开发者和最终用户的利益。 然而如果实现不当,智能合约治理机制可能会带来新的风险。 一个可能的情景是,攻击者通过闪电贷获取巨大的投票权(以持有的代币数量衡量),并推动通过一项恶意提案。
防止与链上治理相关问题的一种方法是使用时间锁opens in a new tab。 时间锁阻止智能合约执行某些操作,直到经过特定的时间长度。 其他策略包括根据每个代币锁定的时间长短为其分配“投票权重”,或者检测一个地址在历史时期(例如,过去的 2-3 个区块)而不是当前区块的投票权。 这两种方法都减少了快速累积投票权以影响链上投票的可能性。
关于设计安全的治理系统opens in a new tab、DAO 中的不同投票机制opens in a new tab以及利用 DeFi 的常见 DAO 攻击媒介opens in a new tab的更多信息,请参阅分享的链接。
8. 将代码复杂性降至最低
传统的软件开发者熟悉 KISS(“保持简单、保持愚蠢”)原则,该原则建议不要将不必要的复杂性带入到软件设计中。 这与长期以来的见解“复杂的系统有着复杂的失败方式”不谋而合,而且复杂系统更容易出现代价高昂的错误。
编写智能合约时简洁化尤其重要,因为智能合约有可能控制大量的价值。 编写智能合约时,实现简洁的一个技巧是尽可能重用现有库,例如 OpenZeppelin Contractsopens in a new tab。 因为开发者对这些库已经进行了广泛的审计和测试,使用它们会减少从零开始编写新功能时引入漏洞的几率。
另一个常见的建议是通过将业务逻辑拆分到多个合约中,编写小型函数并保持合约模块化。 编写更简单的代码不仅仅会减少智能合约中的攻击面,还让推理整个系统的正确性并及早发现可能的设计错误变得更加容易。
9. 防范常见的智能合约漏洞
重入
以太坊虚拟机不允许并发,这意味着消息调用中涉及的两个合约不能同时运行。 外部调用暂停调用合约的执行和内存,直到调用返回,此时执行正常进行。 此过程可以正式描述为将控制流opens in a new tab转移到另一个合约。
尽管这种转向大多数情况下没有危害,但将控制流转向不受信任的合约可能引起问题,例如重入攻击。 当恶意合约在初始函数调用完成之前回调有漏洞的合约时,就会发生重入攻击。 这类攻击最好用一个例子来解释。
考虑一个简单的智能合约(“Victim”),它允许任何人存入和提取以太币:
1// 此合约存在漏洞。请勿在生产环境中使用23contract Victim {4 mapping (address => uint256) public balances;56 function deposit() external payable {7 balances[msg.sender] += msg.value;8 }910 function withdraw() external {11 uint256 amount = balances[msg.sender];12 (bool success, ) = msg.sender.call.value(amount)("");13 require(success);14 balances[msg.sender] = 0;15 }16}显示全部此合约公开了一个 withdraw() 函数,允许用户提取先前存入合约的 ETH。 当处理提款时,合约执行以下操作:
- 检查用户的以太币余额
- 将资金发送给调用地址
- 将其余额重置为 0,防止用户再提取
Victim 合约中的 withdraw() 函数遵循“检查-交互-影响”模式。 它会_检查_执行所需条件是否满足(即用户有正的 ETH 余额),并在应用交易_影响_(即减少用户余额)之前,通过向调用者地址发送 ETH 来执行_交互_。
如果从外部持有账户 (EOA) 调用 withdraw(),该函数会按预期执行:msg.sender.call.value() 会将 ETH 发送给调用者。 但是,如果 msg.sender 是一个调用 withdraw() 的智能合约账户,使用 msg.sender.call.value() 发送资金也会触发存储在该地址的代码运行。
假设以下是部署在合约地址的代码:
1 contract Attacker {2 function beginAttack() external payable {3 Victim(victim_address).deposit.value(1 ether)();4 Victim(victim_address).withdraw();5 }67 function() external payable {8 if (gasleft() > 40000) {9 Victim(victim_address).withdraw();10 }11 }12}显示全部此合约执行下面三项操作:
- 接受来自另一帐户(如攻击者的外部帐户)的存款
- 将 1 个以太币存入 Victim 合约
- 提取存储在该智能合约中的 1 个以太币
这本身没有问题,但如果传入的 msg.sender.call.value 剩余的燃料超过 40,000,Attacker 合约中的另一个函数会再次调用 Victim 合约中的 withdraw() 函数。 这使得 Attacker 能够在 withdraw 的第一次调用完成_之前_重入 Victim 并提取更多资金。 这个循环如下所示:
1- Attacker 的外部帐户使用 1 个以太币调用 `Attacker.beginAttack()`2- `Attacker.beginAttack()` 将 1 个以太币存入 `Victim`3- `Attacker` 调用`Victim` 中的`withdraw()4- `Victim` 检查 `Attacker` 的余额(1 个以太币)5- `Victim` 发送 1 个以太币给 `Attacker`(触发默认函数)6- `Attacker` 再次调用 `Victim.withdraw()`(注意 `Victim` 并没有减少 `Attacker` 第一次提款后的余额)7- `Victim` 检查 `Attacker` 的余额(仍然是 1 个以太币,因为它没有应用第一次调用的效果)8- `Victim` 发送 1 个以太币给 `Attacker`(触发默认函数,让 `Attacker` 可以重入 `withdraw` 函数)9- 这个过程重复进行,直到 `Attacker `耗燃料,此时 `msg.sender.call.value `返回但不会触发额外的提款10- 最后 `Victim` 将第一笔交易(和后续交易)的结果应用于其状态,所以 `Attacker` 的余额被设置为 0显示全部总结起来就是,由于调用者的余额在函数执行完成之前没有设置为 0,所以后续的调用会成功,让调用者可以多次提取他们的余额。 这种攻击可用于耗尽智能合约的资金,就像在 2016 年 DAO 黑客事件opens in a new tab中发生的那样。 正如公开的重入攻击列表opens in a new tab所示,重入攻击至今仍然是智能合约的一个关键问题。
如何防止重入攻击
处理重入的一种方法是遵循检查-影响-交互模式opens in a new tab。 这种模式要求按照以下方式执行函数:最先执行在继续执行函数前执行必要检查的代码,再执行操作合约状态的代码,最后执行与其他合约或外部帐户交互的代码。
下所示的 Victim 合约修订版中使用了检查-影响-交互模式:
1contract NoLongerAVictim {2 function withdraw() external {3 uint256 amount = balances[msg.sender];4 balances[msg.sender] = 0;5 (bool success, ) = msg.sender.call.value(amount)("");6 require(success);7 }8}该合约对用户的余额执行_检查_,应用 withdraw() 函数的_影响_(将用户余额重置为 0),然后继续执行_交互_(将 ETH 发送至用户地址)。 这确保了合约在外部调用之前更新其存储空间,消除了导致第一次攻击的重入攻击的条件。 Attacker 合约仍然可以回调 NoLongerAVictim,但由于 balances[msg.sender] 已被设为 0,额外的提款将引发错误。
另一种方案是使用互斥锁(通常称为“mutex”),它锁定一部分合约状态直到函数调用完成。 这是通过一个布尔变量实现的,该变量在函数执行前设置为 true,在调用完成后恢复为 false。 如下面的例子所示,使用互斥锁可以防止函数在初始调用仍在进行时不受到递归调用,从而有效地阻止重入攻击。
1pragma solidity ^0.7.0;23contract MutexPattern {4 bool locked = false;5 mapping(address => uint256) public balances;67 modifier noReentrancy() {8 require(!locked, "重入被阻止。");9 locked = true;10 _;11 locked = false;12 }13 // 此函数受互斥锁保护,因此来自 `msg.sender.call` 内部的重入调用无法再次调用 `withdraw`。14 // `return` 语句的计算结果为 `true`,但仍会计算修饰符中的 `locked = false` 语句15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {16 require(balances[msg.sender] >= _amount, "没有可供提取的余额。");1718 balances[msg.sender] -= _amount;19 (bool success, ) = msg.sender.call{value: _amount}("");20 require(success);2122 return true;23 }24}显示全部你也可以使用拉取支付opens in a new tab系统,该系统要求用户从智能合约中提取资金,而不是将资金发送到账户的“推送支付”系统。 这样就消除了意外触发未知地址中代码的可能性(还可以防止某些拒绝服务攻击)。
整数下溢和上溢
当算术运算的结果超出可接受的值范围,导致其“滚动”到可表示的最小值,整数溢出发生。 例如,一个 uint8 类型只能存储最大为 2^8-1=255 的值。 导致值高于 255 的算术运算将上溢,并将 uint 重置为 0,这与汽车里程表达到最大里程 (999999) 时重置为 0 的方式类似。
整数下溢的发生原因类似:算术运算的结果低于可接受的范围。 假设您尝试在 uint8 中对 0 进行递减,结果将直接回绕到最大可表示值 (255)。
整数溢出和下溢都会导致合约的状态变量出现意外变化,引发意外的执行。 以下例子说明了攻击者如何利用智能合约的算数溢出执行无效操作:
1pragma solidity ^0.7.6;23// 此合约旨在充当时间金库。4// 用户可以向此合约存入资金,但至少一周内不能提取。5// 用户还可以将等待时间延长至超过 1 周的等待期。67/*81. 部署 TimeLock92. 用 TimeLock 的地址部署 Attack103. 调用 Attack.attack 并发送 1 个以太币。您将能够立即11 提取您的以太币。1213发生了什么?14Attack 导致 TimeLock.lockTime 上溢,从而能够在 1 周的等待期之前15提取资金。16*/1718contract TimeLock {19 mapping(address => uint) public balances;20 mapping(address => uint) public lockTime;2122 function deposit() external payable {23 balances[msg.sender] += msg.value;24 lockTime[msg.sender] = block.timestamp + 1 weeks;25 }2627 function increaseLockTime(uint _secondsToIncrease) public {28 lockTime[msg.sender] += _secondsToIncrease;29 }3031 function withdraw() public {32 require(balances[msg.sender] > 0, "资金不足");33 require(block.timestamp > lockTime[msg.sender], "锁仓时间未到");3435 uint amount = balances[msg.sender];36 balances[msg.sender] = 0;3738 (bool sent, ) = msg.sender.call{value: amount}("");39 require(sent, "以太币发送失败");40 }41}4243contract Attack {44 TimeLock timeLock;4546 constructor(TimeLock _timeLock) {47 timeLock = TimeLock(_timeLock);48 }4950 fallback() external payable {}5152 function attack() public payable {53 timeLock.deposit{value: msg.value}();54 /*55 如果 t = 当前锁仓时间,那么我们需要找到 x,使得56 x + t = 2**256 = 057 所以 x = -t58 2**256 = type(uint).max + 159 所以 x = type(uint).max + 1 - t60 */61 timeLock.increaseLockTime(62 type(uint).max + 1 - timeLock.lockTime(address(this))63 );64 timeLock.withdraw();65 }66}显示全部如何防止整数溢出和下溢
从 0.8.0 版开始,Solidity 编译器禁用导致整数下溢和溢出的代码。 但是,使用较低编译器版本编译的合约,应在涉及算术运算的函数上执行检查,或使用检查下溢/上溢的库(例如,SafeMathopens in a new tab)。
预言机操纵
预言机从链下获取信息并将其发送到链上供智能合约使用。 通过预言机,你可以设计出和链下系统(例如资本市场)交互的智能合约,极大地拓展它们的应用。
但如果预言机损坏并向链上发送错误信息,智能合约将基于错误的输入执行,这会造成问题。 这就是“预言机问题”的根源,它涉及确保区块链预言机提供准确、最新、即时的信息。
相关的安全问题就是利用链上预言机(例如去中心化交易所)获取一种资产的现货价格。 去中心化金融 (DeFi) 行业的借贷平台通常会这样做,以确定用户抵押品的价值,从而决定他们可以借入多少资金。
去中心化交易所 (DEX) 的价格往往是准确的,很大程度上源于套利者的套利行为帮助市场恢复平价。 然而,这样容易受到操纵,尤其当链上预言机根据历史交易模式计算资产价格时(通常是这种情况)。
例如,攻击者可以在与你的借贷合约交互前,通过获得闪电贷人为拉高资产的现货价格。 在向去中心化交易所 (DEX) 查询资产价格时,将返回一个高于正常水平的值(由于攻击者对大宗“买入订单”影响了资产的需求),这样攻击者就可以借来比原本更多的资金。 这种“闪电贷攻击”一直在利用对去中心化金融应用程序之间的价格预言机的依赖,使许多协议遭受了数百万美元的资金损失。
如何防止预言机操纵
避免预言机操纵opens in a new tab的最低要求是使用去中心化的预言机网络,该网络从多个来源查询信息,以避免单点故障。 在大多数情况下,去中心化预言机有內置的加密经济学激励机制,鼓励预言机节点报告正确的信息,使它们比中心化预言机更安全。
如果你打算通过查询链上预言机获得资产价格,考虑使用实施了时间加权平均价格 (TWAP) 机制的预言机。 时间加权平均价格 (TWAP) 预言机opens in a new tab在两个不同的时间点(您可以修改)查询资产的价格,并根据获得的平均值计算现货价格。 选择较长的时间段可以保护协议免受价格操纵,因为最近执行的大宗订单无法影响资产价格。
面向开发者的智能合约安全资源
用于分析智能合约和验证代码正确性的工具
-
测试工具和库 - 用于对智能合约执行单元测试、静态分析和动态分析的行业标准工具和库的集合。
-
形式化验证工具 - 用于验证智能合约功能正确性和检查不变量的工具。
-
智能合约审计服务 - 为以太坊开发项目提供智能合约审计服务的组织列表。
-
漏洞赏金平台 - 用于协调漏洞赏金和奖励负责任地披露智能合约中关键漏洞的平台。
-
Fork Checkeropens in a new tab - 一个免费的在线工具,用于检查有关分叉合约的所有可用信息。
-
ABI 编码器opens in a new tab - 一项免费的在线服务,用于对您的 Solidity 合约函数和构造函数参数进行编码。
-
Aderynopens in a new tab - Solidity 静态分析器,遍历抽象语法树 (AST) 以查明可疑漏洞,并以易于使用的 Markdown 格式打印问题。
智能合约监控工具
- Tenderly 实时警报opens in a new tab - 一个用于在您的智能合约或钱包上发生异常或意外事件时获取实时通知的工具。
安全管理智能合约的工具
-
Safeopens in a new tab - 在以太坊上运行的智能合约钱包,需要至少 N 人中的 M 人批准才能执行交易 (M-of-N)。
-
OpenZeppelin Contractsopens in a new tab - 用于实现管理功能的合约库,包括合约所有权、升级、访问控制、治理、暂停功能等。
智能合约审计服务
-
ConsenSys Diligenceopens in a new tab - 智能合约审计服务,帮助整个区块链生态系统中的项目确保其协议已准备好启动并为保护用户而构建。
-
CertiKopens in a new tab - 区块链安全公司,率先在智能合约和区块链网络上使用尖端的形式化验证技术。
-
Trail of Bitsopens in a new tab - 网络安全公司,将安全研究与攻击者思维相结合,以降低风险并加固代码。
-
PeckShieldopens in a new tab - 区块链安全公司,为整个区块链生态系统的安全性、隐私性和可用性提供产品和服务。
-
QuantStampopens in a new tab - 通过安全和风险评估服务促进区块链技术成为主流的审计服务。
-
OpenZeppelinopens in a new tab - 智能合约安全公司,为分布式系统提供安全审计。
-
Runtime Verificationopens in a new tab - 安全公司,专门从事智能合约的形式化建模与验证。
-
Hackenopens in a new tab - Web3 网络安全审计公司,为区块链安全提供全方位解决方案。
-
Nethermindopens in a new tab - Solidity 和 Cairo 审计服务,确保智能合约的完整性以及以太坊和 Starknet 上用户的安全。
-
HashExopens in a new tab - HashEx 专注于区块链和智能合约审计,以确保加密货币的安全,提供智能合约开发、渗透测试、区块链咨询等服务。
-
Code4renaopens in a new tab - 竞争性审计平台,激励智能合约安全专家查找漏洞,帮助提高 web3 的安全性。
-
CodeHawksopens in a new tab - 竞争性审计平台,为安全研究者举行智能合约审计比赛。
-
Cyfrinopens in a new tab - Web3 安全巨头,通过产品和智能合约审计服务孵化加密货币安全。
-
ImmuneBytesopens in a new tab - Web3 安全公司,通过经验丰富的审计员团队和一流的工具,为区块链系统提供安全审计。
-
Oxorioopens in a new tab - 智能合约审计和区块链安全服务,为加密货币公司和 DeFi 项目提供以太坊虚拟机、Solidity、零知识证明、跨链技术方面的专业知识。
-
Inferenceopens in a new tab - 安全审计公司,专注于为基于以太坊虚拟机的区块链进行智能合约审计。 多亏他们的审计专家,他们发现了潜在问题并提出了可行的解决方案,在部署之前进行修复。
漏洞赏金平台
-
Immunefiopens in a new tab - 智能合约和 DeFi 项目的漏洞赏金平台,安全研究人员在该平台上审查代码、披露漏洞、获得报酬并使加密货币更加安全。
-
HackerOneopens in a new tab - 漏洞协调和漏洞赏金平台,将企业与渗透测试人员和网络安全研究人员连接在一起。
-
HackenProofopens in a new tab - 针对加密货币项目(DeFi、智能合约、钱包、中心化交易所等)的专业级漏洞赏金平台,借助这一平台,安全专家可提供漏洞诊断服务,研究人员会因为提供经过验证的相关漏洞报告获得报酬。
-
Sherlockopens in a new tab - Web3 中的智能合约安全性承销商,通过智能合约管理审计人员的报酬,以确保相关漏洞得到公平的支付。
-
CodeHawksopens in a new tab - 竞争性漏洞奖金平台,供审计人员参与安全竞赛和挑战,并且(很快)能够参与他们自己的私人审计。
关于已知智能合约漏洞和利用的出版物{#common-smart-contract-vulnerabilities-and-exploits}
-
ConsenSys:智能合约已知攻击opens in a new tab - 针对最重要的合约漏洞提供适合初学者的解释,多数案例提供了代码示例。
-
SWC 注册表opens in a new tab - 适用于以太坊智能合约的常见缺陷枚举 (CWE) 的精选项目列表。
-
Rektopens in a new tab - 定期更新的著名加密货币领域黑客攻击和漏洞利用刊物,并提供详细的事后分析报告。
学习智能合约安全的挑战
-
Awesome BlockSec CTFopens in a new tab - 精选的区块链安全“战争游戏”、挑战、夺旗赛opens in a new tab以及解题报告列表。
-
Damn Vulnerable DeFiopens in a new tab - 通过实战演练学习 DeFi 智能合约的攻击性安全,并培养漏洞搜查和安全审计方面的技能。
-
Ethernautopens in a new tab - 基于 Web3 和 Solidity 的“战争游戏”,其中每个级别都是一个需要“攻破”的智能合约。
-
HackenProof x HackTheBoxopens in a new tab - 以奇幻冒险为背景的智能合约黑客挑战。 成功完成挑战还有机会参与私人漏洞奖金项目。
保护智能合约的最佳实践
-
ConsenSys:以太坊智能合约安全最佳实践opens in a new tab - 保护以太坊智能合约安全的完整指南列表。
-
Nascent:简单安全工具箱opens in a new tab - 智能合约开发的实用安全指南和检查清单的集合。
-
Solidity 模式opens in a new tab - 面向智能合约编程语言 Solidity 的安全模式和最佳实践实用合集。
-
Solidity 文档:安全注意事项opens in a new tab - 使用 Solidity 编写安全智能合约的指南。
-
智能合约安全验证标准opens in a new tab - 为开发者、架构师、安全审核者和供应商创建的、由十四部分组成的检查清单,旨在确立智能合约的安全性标准。
-
学习智能合约安全与审计opens in a new tab - 智能合约安全与审计终极课程,专为寻求提升其安全性最佳做法和希望成为安全研究者的智能合约开发者创建。