跳转至主要内容
Change page

智能合约安全性

上次修改时间: @EffectChen(opens in a new tab), 2024年5月22日

智能合约极为灵活,能够控制大量的价值和数据,并在区块链上运行基于代码的不可改变逻辑。 因而,一个由去信任的去中心化应用程序构成的生态系统应运而生且充满活力,它具备了许多传统系统所没有的优势。 同时,这也给攻击者提供了利用智能合约中的漏洞来获利的机会。

公共区块链(比如以太坊)使智能合约的安全性问题变的更加复杂。 已部署的合约代码通常无法更改因而不能给安全问题打补丁,并且由于这种不可变性,从智能合约中盗取的资产极难追踪并且绝大多数无法挽回。

虽然统计数据有所差异,但据估计,由于智能合约的安全缺陷而被盗窃或丢失的资产总额肯定超过了 10 亿美元。 其中包括几次著名事件,比如 DAO 攻击事件(opens in a new tab)(360 万个以太币被盗,按照当前价格计算总金额超过 10 亿美元)、Parity 多重签名钱包攻击事件(opens in a new tab)(黑客窃取了 3000 万美元)以及 Parity 钱包冻结问题(opens in a new tab)(价值超过 3 亿美元的以太币遭到永久锁定)。

上述几个事件迫使开发者必须付诸努力,构建安全、稳健、恢复力强的智能合约。 智能合约安全性是每个开发者都需要学习和研究的严肃问题。 本指南将介绍针对以太坊开发者的安全性注意事项,并研究增强智能合约安全性的资源。

前言

在开始研究安全性问题之前,请确保自己已经熟悉智能合约开发的基础知识

安全以太坊智能合约的构建准则

1. 设计合理的访问控制

在智能合约中,带有 publicexternal 标记的函数可以被任何外部帐户 (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;
2
3contract VendingMachine {
4 address owner;
5 error Unauthorized();
6 function buy(uint amount) public payable {
7 if (amount > msg.value / 2 ether)
8 revert("Not enough Ether provided.");
9 // Perform the purchase.
10 }
11 function withdraw() public {
12 if (msg.sender != owner)
13 revert Unauthorized();
14
15 payable(msg.sender).transfer(address(this).balance);
16 }
17}
显示全部

3. 测试智能合约并验证代码正确性

鉴于在以太坊虚拟机中运行的代码的不可变性,智能合约在开发阶段需要更高水平的质量评估。 对合约进行大量测试并观察是否存在任何意外结果,将显著增强合约的安全性并为用户提供长远保护。

常用办法编写小单元测试,这些测试使用预计合约从用户处接收的模拟数据。 单元测试能够测试某些函数的功能并确保智能合约按预期运行。

遗憾的是,单独使用单元测试对提高智能合约的安全性效果甚微。 单元测试也许可以证明函数对于模拟数据正确执行,但单元测试的有效性受限于编写的测试。 这就意味着很难检测到威胁智能合约安全性的边缘情况和漏洞。

更好的方法是将单元测试与基于属性的测试相结合,后者是通过静态和动态分析进行的。 静态分析依赖于底层的表示(例如控制流程图(opens in a new tab)抽象语法树(opens in a new tab))分析可达到的程序状态和执行路径。 相比之下,动态分析技术(例如模糊测试)用随机输入值执行合约代码,以检测违反安全属性的操作。

形式化验证是另一项验证智能合约安全属性的技术。 与常规测试不同,形式化验证能够确证智能合约中没有错误。 这是通过制定细致描述安全属性的形式化规范并证明智能合约的形式化模型符合这一规范来实现的。

4. 申请代码独立审核

在测试智能合约后,最好请其他人检查源代码是否存在安全问题。 虽然测试无法发现智能合约中的所有缺陷,但进行独立审核能增加发现漏洞的可能性。

审计

进行独立代码审核的方式之一是委托执行智能合约审计。 审计员是确保智能合约安全、没有质量缺陷和设计错误的关键所在。

尽管如此,你也不应将审计看作终极方案。 智能合约审计无法发现所有漏洞并且主要是为了额外增加一轮审核,这有助于检测到开发者在最初的开发和测试中遗漏的问题。 你还应遵循与审计员合作的最佳做法(opens in a new tab)(例如正确记录代码并添加行内注释),让智能合约审计发挥最大作用。

漏洞奖励

执行外部代码审查的另一种方法是设立漏洞奖励计划。 漏洞奖励是一种经济奖励,提供给发现应用程序中漏洞的个人(通常是白帽黑客)。

应用得当,漏洞奖励可以激励黑客群体中的成员检查你的代码是否存在重大缺陷。 一个真实的示例是“无限复制倾向漏洞”,它可以让攻击者在以太坊上运行的二层网络协议 Optimism(opens in a new tab) 上创建无限量的以太币。 幸运的是,一位白帽黑客发现了这一漏洞(opens in a new tab)并告知了以太坊团队,并获得了一大笔报酬(opens in a new tab)

一种实用策略是按有风险资金数额的比例设置漏洞奖励计划的报酬金额。 这种方法被描述成“比例漏洞奖励(opens in a new tab)”,通过提供经济激励让大家负责任地披露而非利用漏洞。

5. 智能合约开发过程中遵循最佳做法

即使审计和漏洞奖励存在,你也有责任编写高质量的代码。 遵循正确的设计和开发流程是良好的智能合约安全性的开端:

  • 将所有代码存放在一个版本控制系统中,例如 git

  • 所有代码修改通过拉取请求进行

  • 确保拉取请求至少有一位独立审核者 — 如果只有你一人完成项目,考虑和其他开发者相互进行代码审核

  • 开发环境下测试、编译、和部署智能合约

  • 在如 Mythril 和 Slither 等基本代码分析工具中运行代码。 理想情况下,应在合并每个拉取请求前进行这一操作,并比较输出中的不同之处

  • 确保代码在编译时没有错误,并且 Solidity 编译器没有发出警告

  • 正确记录代码(使用 NatSpec(opens in a new tab)),并用易于理解的语言描述合约架构的细节。 这将使其他人更容易审计和审核你的代码。

6. 实施可靠的灾难恢复计划

设计安全的访问控制、使用函数修改器以及其他建议能够提高智能合约的安全性,但这些并不能排除恶意利用的可能性。 构建安全的智能合约需要“做好失败准备”,并制定好应变计划有效地应对攻击。 适当的灾难恢复计划应包括以下部分或全部内容:

合约升级

虽然以太坊智能合约默认是不可变的,但通过使用升级模式可以实现一定程度的可变性。 如果重大缺陷导致合约不可用并且部署新逻辑是最可行的选择,有必要升级合约。

合约升级机制的原理有所不同,但“代理模式”是智能合约升级最常见的方法之一。 代理模式将应用程序的状态和逻辑拆分到两个合约中。 第一个合约(称为“代理合约”)存储状态变量(如用户余额),而第二个合约(称为"逻辑合约")存放执行合约函数的代码。

帐户与代理合约互动,代理合约通过delegatecall()(opens in a new tab)的低级调用将所有功能调用分发给逻辑合约。 与普通的消息调用不同,delegatecall() 确保在逻辑的合约地址上运行的代码是在调用合约的语境下执行。 这意味着逻辑合约将始终写入代理的存储空间(而非自身存储空间),并且 msg.sendermsg.value 的原始值保持不变。

将调用委托给逻辑合约需要将其地址存储在代理合约的存储空间。 因此,升级合约的逻辑就相当于部署另一个逻辑合约并在代理合约中存储新的地址。 由于对代理合约的后续调用会自动传送到新的逻辑合约,因此你“升级”了合约,但实际上并未修改代码。

更多关于升级合约的信息

紧急停止

如上所述,大量审计和测试不可能发现智能合约中的所有漏洞。 无法修补在部署后出现的代码漏洞,因为你无法更改运行在智能合约中的代码。 而且,升级机制(如代理模式)可能需要时间来实现(它们往往需要多方批准),这只会给攻击者更多的时间来造成更大的破坏。

这种情况下,核心方案是实施一种“紧急停止”功能,阻止对合约中有漏洞的函数的调用。 紧急停止通常由以下几部分组成:

  1. 表明智能合约是否处在停止状态的全局布尔变量。 在设置合约时该变量设为 false,但在合约停止后将回滚为 true

  2. 执行过程中引用该布尔变量的函数。 此类函数在智能合约没有停止时可以访问,而当紧急停止功能触发后则无法访问。

  3. 可以访问紧急停止功能的实体,可将布尔变量设置为 true。 为防止恶意行为,对此功能的调用可以限制给一个可信地址(如合约所有者)。

一旦合约操作触发紧急停止,某些函数将无法调用。 这是通过把一些函数包装在引用该全局变量的修改器中实现的。 以下示例(opens in a new tab)描述了该模式在合约中的实现:

1// 本代码未经专业审计,对安全性和正确性不做任何承诺。 如需使用,风险自负。
2
3contract EmergencyStop {
4
5 bool isStopped = false;
6
7 modifier stoppedInEmergency {
8 require(!isStopped);
9 _;
10 }
11
12 modifier onlyWhenStopped {
13 require(isStopped);
14 _;
15 }
16
17 modifier onlyAuthorized {
18 // Check for authorization of msg.sender here
19 _;
20 }
21
22 function stopContract() public onlyAuthorized {
23 isStopped = true;
24 }
25
26 function resumeContract() public onlyAuthorized {
27 isStopped = false;
28 }
29
30 function deposit() public payable stoppedInEmergency {
31 // Deposit logic happening here
32 }
33
34 function emergencyWithdraw() public onlyWhenStopped {
35 // Emergency withdraw happening here
36 }
37}
显示全部
复制

以上示例展示了紧急停止的基本特点:

  • 布尔值 isStopped 开始时求值为 false,但当合约进入紧急模式时求值为 true

  • 函数修改器 onlyWhenStoppedstoppedInEmergency 检查 isStopped 变量。 stoppedInEmergency 用于控制在合约有漏洞时应该无法访问的函数(如 deposit())。 对这些函数的调用将仅仅进行回滚而已。

onlyWhenStopped 用于在紧急情况下应该可调用的函数(如 emergencyWithdraw())。 此类函数可以帮助解决问题,因此它们不在“受限制函数”之列。

紧急停止功能的应用,为处理智能合约中的严重漏洞提供了一种有效的权宜之计。 然而,这也意味着用户更需要相信开发者不会为自身利益激活这一功能。 为此,将紧急停止的控制权去中心化,使其受到链上投票机制、时间锁的约束或者需要来自多重签名钱包的批准,都是潜在的解决方案。

事件监测

事件(opens in a new tab)允许用户跟踪对智能合约函数的调用并监测状态变量的变化。 最理想的做法是将智能合约编写为能够在某一方采取对安全至关重要的操作(如提取资金)时发出一个事件。

记录事件并进行链下监测,可以深入了解合同的运作情况,有助于更快地发现恶意行为。 这意味着你的团队可以更快地应对黑客攻击并采取行动减轻对用户的影响,如暂停函数或进行升级。

你也可以选择一种现成的监测工具,只要有人与你的合约交互,就会自动转发警报。 这些工具将允许你根据不同的触发器创建自定义警报,如交易量、函数调用的频率或相关具体函数。 例如,你可以编写一个警报,当单笔交易的提款金额超过特定阈值时触发。

7. 设计安全的治理系统

你可能想要通过将核心智能合约的控制权转交给社区成员来去中心化你的应用。 在这种情况下,智能合约系统将包括一个治理模块 — 一种允许社区成员通过链上治理系统批准管理行为的机制。 例如,将代理合约升级为新实现的提案可能由代币持有人投票。

去中心化治理可能是有益的,特别是因为它符合开发者和最终用户的利益。 然而如果实现不当,智能合约治理机制可能会带来新的风险。 一种可能的场景是,攻击者通过取得闪电贷获得了很大的投票权(以持有的代币数量衡量)并通过一条恶意提案。

防止与链上治理有关的问题的一种方法是使用时间锁(opens in a new tab)。 时间锁阻止智能合约执行某些操作,直到经过特定的时间长度。 其他策略包括根据每个代币锁定的时间长短为其分配“投票权重”,或者检测一个地址在历史时期(例如,过去的 2-3 个区块)而不是当前区块的投票权。 这两种方法都减少了快速累积投票权以影响链上投票的可能性。

更多关于设计安全的治理系统(opens in a new tab)去中心化自治组织中的不同投票机制(opens in a new tab)的信息。

8. 将代码的复杂性降到最低

传统的软件开发者熟悉 KISS(“保持简单、保持愚蠢”)原则,该原则建议不要将不必要的复杂性带入到软件设计中。 这与长期以来的见解“复杂的系统有着复杂的失败方式”不谋而合,而且复杂系统更容易出现代价高昂的错误。

编写智能合约时简洁化尤其重要,因为智能合约有可能控制大量的价值。 实现简洁化的一个窍门是,编写智能合约时在允许的情况下重用已存在的库,例如 OpenZeppelin Contracts(opens in a new tab)。 因为开发者对这些库已经进行了广泛的审计和测试,使用它们会减少从零开始编写新功能时引入漏洞的几率。

另一个常见的建议是通过将业务逻辑拆分到多个合约中,编写小型函数并保持合约模块化。 编写更简单的代码不仅仅会减少智能合约中的攻击面,还让推理整个系统的正确性并及早发现可能的设计错误变得更加容易。

9. 防范常见的智能合约漏洞

重入攻击

以太坊虚拟机不允许并发,这意味着消息调用中涉及的两个合约不能同时运行。 外部调用暂停调用合约的执行和内存,直到调用返回,此时执行正常进行。 该过程可以正式描述为将控制流(opens in a new tab)转向另一个合约。

尽管这种转向大多数情况下没有危害,但将控制流转向不受信任的合约可能引起问题,例如重入攻击。 当恶意合约在初始函数调用完成之前回调有漏洞的合约时,就会发生重入攻击。 这类攻击最好用一个例子来解释。

考虑一个简单的智能合约(“Victim”),它允许任何人存入和提取以太币:

1// This contract is vulnerable. Do not use in production
2
3contract Victim {
4 mapping (address => uint256) public balances;
5
6 function deposit() external payable {
7 balances[msg.sender] += msg.value;
8 }
9
10 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() 函数,允许用户提取先前存入合约的以太币。 当处理提款时,合约执行以下操作:

  1. 检查用户的以太币余额
  2. 将资金发送给调用地址
  3. 将其余额重置为 0,防止用户再提取

Victim 合约中的 withdraw() 函数遵循“检查-交互-效果”模式。 它检查执行所需的条件是否满足(例如,用户的以太币余额是否为正值)并通过向调用者的地址发送以太币来执行交互,然后再应用交易的效果(例如减少用户的余额)。

如果从外部帐户调用 withdraw(),该函数将按预期执行:msg.sender.call.value() 向调用方发送以太币。 然而,如果 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 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(victim_address).withdraw();
10 }
11 }
12}
显示全部
复制

此合约执行下面三项操作:

  1. 接受来自另一帐户(如攻击者的外部帐户)的存款
  2. 将 1 个以太币存入 Victim 合约
  3. 提取存储在该智能合约中的 1 个以太币

这里没有什么问题,只是 Attacker 有另一个函数,如果传入的 msg.sender.call.value 调用剩余的燃料超过 40000,它就再次调用 Victim 中的 withdraw() 函数。 这使得 Attacker 能够重入 Victim 合约并在第一次调用 withdraw 函数结束之前提取更多资金。 这个循环如下所示:

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)并继续执行交互(将以太币发送到用户的地址)。 这确保了合约在外部调用之前更新其存储空间,消除了导致第一次攻击的重入攻击的条件。 Attacker 合约可能仍然可以回调 NoLongerAVictim,但由于 balances[msg.sender] 已设置为 0,额外的提取将引发错误。

另一种方案是使用互斥锁(通常称为“mutex”),它锁定一部分合约状态直到函数调用完成。 互斥锁是通过布尔变量实现的,该变量在函数执行之前设置为 true,在调用完成后回滚为 false。 如下面的例子所示,使用互斥锁可以防止函数在初始调用仍在进行时不受到递归调用,从而有效地阻止重入攻击。

1pragma solidity ^0.7.0;
2
3contract MutexPattern {
4 bool locked = false;
5 mapping(address => uint256) public balances;
6
7 modifier noReentrancy() {
8 require(!locked, "Blocked from reentrancy.");
9 locked = true;
10 _;
11 locked = false;
12 }
13 // This function is protected by a mutex, so reentrant calls from within `msg.sender.call` cannot call `withdraw` again.
14 // The `return` statement evaluates to `true` but still evaluates the `locked = false` statement in the modifier
15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {
16 require(balances[msg.sender] >= _amount, "No balance to withdraw.");
17
18 balances[msg.sender] -= _amount;
19 bool (success, ) = msg.sender.call{value: _amount}("");
20 require(success);
21
22 return true;
23 }
24}
显示全部
复制

还可以使用拉取支付(opens in a new tab) 系统,该系统要求用户从智能合约中提取资金,而不是使用将资金发送到帐户的“推送支付”系统。 这样就消除了意外触发未知地址中代码的可能性(还可以防止某些拒绝服务攻击)。

整数下溢和溢出

当算术运算的结果超出可接受的值范围,导致其“滚动”到可表示的最小值,整数溢出发生。 例如,uint8 只能存储最大为 2^-1=255 的值。 算术运算的结果如果大于 255,即溢出并重置 Uint0,这类似于汽车里程表,一旦达到最大里程 (999999) 示数就重置为 0。

整数下溢发生的原因类似:算术运算的结果小于可接受的范围。 比方说,你尝试减少 uint8 中的一个0,结果将只会滚动到最大的可表示值 (255)。

整数溢出和下溢都会导致合约的状态变量出现意外变化,引发意外的执行。 以下例子说明了攻击者如何利用智能合约的算数溢出执行无效操作:

1pragma solidity ^0.7.6;
2
3// This contract is designed to act as a time vault.
4//用户可以向合约里存款但至少在一周内无法提款。
5//用户还可以延长 1 周的等待期。
6
7/*
81. 部署 TimeLock
92. 使用 TimeLock 地址部署 Attack
103. 调用 Attack.attack,发送 1 个以太币。 你将能够立即
11 提取你的以太币。
12
13发生了什么?
14攻击造成了 TimeLock 溢出,并且能够在 1 周的等待期之
15前提款。
16*/
17
18contract TimeLock {
19 mapping(address => uint) public balances;
20 mapping(address => uint) public lockTime;
21
22 function deposit() external payable {
23 balances[msg.sender] += msg.value;
24 lockTime[msg.sender] = block.timestamp + 1 weeks;
25 }
26
27 function increaseLockTime(uint _secondsToIncrease) public {
28 lockTime[msg.sender] += _secondsToIncrease;
29 }
30
31 function withdraw() public {
32 require(balances[msg.sender] > 0, "Insufficient funds");
33 require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
34
35 uint amount = balances[msg.sender];
36 balances[msg.sender] = 0;
37
38 (bool sent, ) = msg.sender.call{value: amount}("");
39 require(sent, "Failed to send Ether");
40 }
41}
42
43contract Attack {
44 TimeLock timeLock;
45
46 constructor(TimeLock _timeLock) {
47 timeLock = TimeLock(_timeLock);
48 }
49
50 fallback() external payable {}
51
52 function attack() public payable {
53 timeLock.deposit{value: msg.value}();
54 /*
55 if t = current lock time then we need to find x such that
56 x + t = 2**256 = 0
57 so x = -t
58 2**256 = type(uint).max + 1
59 so x = type(uint).max + 1 - t
60 */
61 timeLock.increaseLockTime(
62 type(uint).max + 1 - timeLock.lockTime(address(this))
63 );
64 timeLock.withdraw();
65 }
66}
显示全部
如何防止整数溢出和下溢

从 0.8.0 版开始,Solidity 编译器禁用导致整数下溢和溢出的代码。 然而,用较低编译器版本编译的合约应当对涉及算术运算的函数执行检查,或者使用检查是否发生下溢/溢出的库(例如 SafeMath(opens in a new tab))。

预言机操纵

预言机获取链下信息并将这些信息发送到链上供智能合约使用。 通过预言机,你可以设计出和链下系统(资本市场)交互的智能合约,极大地拓展它们的应用。

但如果预言机损坏并向链上发送错误信息,智能合约将基于错误的输入执行,这会造成问题。 这就是“预言机问题”的根源,它涉及确保区块链预言机提供准确、最新、即时的信息。

相关的安全问题就是利用链上预言机(例如去中心化交易所)获取一种资产的现货价格。 去中心化金融 (DeFi) 行业中的借贷平台经常利用这种方法确定用户抵押品的价值,进而确定他们能借入多少。

去中心化交易所 (DEX) 的价格往往是准确的,很大程度上源于套利者的套利行为帮助市场恢复平价。 然而,去中心化交易所的价格容易受到操纵,尤其当链上预言机根据历史交易模式计算资产价格时(通常是这种情况)。

例如,攻击者可以在与你的借贷合约交互前,通过获得闪电贷人为拉高资产的现货价格。 在向去中心化交易所 (DEX) 查询资产价格时,将返回一个高于正常水平的值(由于攻击者对大宗“买入订单”影响了资产的需求),这样攻击者就可以借来比原本更多的资金。 这种“闪电贷攻击”一直在利用对去中心化金融应用程序之间的价格预言机的依赖,使许多协议遭受了数百万美元的资金损失。

如何防止预言机操纵

避免预言机操纵的最低要求是,使用从多种来源查询信息的去中心化预言机网络,以避免单点故障。 在大多数情况下,去中心化预言机有內置的加密经济学激励机制,鼓励预言机节点报告正确的信息,使它们比中心化预言机更安全。

如果你打算通过查询链上预言机获得资产价格,考虑使用实施了时间加权平均价格 (TWAP) 机制的预言机。 时间加权平均价格预言机(opens in a new tab)查询资产在两个不同时间点(可以修改)的价格,并计算出基于所得平均值的现货价格。 选择较长的时间段可以保护协议免受价格操纵,因为最近执行的大宗订单无法影响资产价格。

面向开发者的智能合约安全性资源

用于分析智能合约和验证代码正确性的工具

  • 测试工具和程序库 - 为智能合约进行单元测试、静态分析和动态分析的行业标准工具和程序库集合。

  • 形式化验证工具 - 用于验证智能合约中的函数正确性和检查不变量的工具。

  • 智能合约审计服务 - 为以太坊开发项目提供智能合约审计服务的组织的列表。

  • 漏洞奖励平台 - 协调漏洞奖励并对发现智能合约中重大漏洞的负责人进行奖励的平台。

  • Fork Checker(opens in a new tab) - 免费的在线工具,用于检查所有关于分叉合同的现有信息。

  • ABI 编码器(opens in a new tab) - 免费在线服务,用于编码你的 Solidity 合约函数和构造函数参数。

智能合约监测工具

智能合约的安全管理工具

智能合约审计服务

漏洞奖励平台

已知智能合约漏洞及利用情况的刊物

智能合约安全学习难点

确保智能合约安全的最佳做法

智能合约安全性教程

  • 如何编写安全的智能合约

  • 如何使用 Slither 查找智能合约漏洞

  • 如何使用 Manticore 查找智能合约漏洞

  • 智能合约安全性准则

  • 如何安全整合代币合约与任意代币

本文对你有帮助吗?