主页 > imtoken安卓官方版 > 智能合约安全
智能合约安全
查看贡献者
编辑页面
以太坊智能合约非常灵活。 它能够存储大量虚拟货币(超过 10 亿美元),并针对先前部署的智能合约运行不可修改的代码。 虽然这创造了一个充满活力和创造性的生态系统,但它包含的无需信任、相互关联的智能合约也吸引了攻击者利用智能合约中的漏洞和以太坊中的未知错误来获利。 智能合约代码通常无法修改以修复安全漏洞,因此智能合约被盗资产无法找回,被盗资产极难追踪。 由于智能合约问题而被盗或丢失的总价值很容易超过 10 亿美元。 一些因智能合约编码错误造成的巨大经济损失的例子:
先决条件
本章将介绍智能合约的安全问题,以确保您在处理安全问题之前熟悉智能合约。
如何编写更安全的智能合约代码
在向主网启动任何代码之前,重要的是要采取充分的预防措施来保护委托给您的智能合约的任何有价值的东西。 在本文中,我们讨论了一些特定的攻击,提供资源以了解有关这些类型攻击的更多信息,并为您提供一些基本工具和最佳实践,以确保您的合约正确、安全地运行。
审计不是一个完美的解决方案
几年前,编写、编译、测试和部署智能合约的工具还很不成熟,很多项目都是随便写了Solidity代码,然后交给reviewer,review者来review代码,确保安全合规. 按预期运行。 2020 年,编写 Solidity 代码的开发流程和工具都有了明显的改进,不仅让项目更容易管理,也成为项目安全的一部分。 仅在项目结束时审核您的智能合约已不足以成为项目的唯一安全考虑因素。 安全来自于正确的设计和开发过程,因此在编写第一行智能合约代码之前就应该考虑安全性。
智能合约开发流程
最低安全性:
上面的这些项目是编写智能合约的良好开端,但在编写代码时还有更多需要注意的地方。 更多项目及其详细解释以太坊合约地址能否修改,请参考DeFiSafety提供的流程质量检查表。 DefiSafety 是一项非官方的公共服务,发布各种大型公共以太坊去中心化应用程序的评论。 DeFiSafete 对项目的部分安全评级等级包括该项目是否符合质量检查表。 请遵循以下审核流程:
攻击和漏洞
现在您正在使用高效的开发流程编写 Solidity 代码,让我们来看看一些常见的 Solidity 漏洞问题。
重入攻击
编写智能合约代码时应该考虑的最大和最重要的安全问题是重入攻击。 虽然以太坊虚拟机不能同时运行多个合约,但是一个合约可以调用另一个合约来暂停一个合约的执行和内存状态,直到它再次被调用。 此时代码会继续正常执行。 挂起和重新启动的过程会产生一个称为“重入攻击”的漏洞。
这是一个易受重入影响的合约:
1// THIS CONTRACT HAS INTENTIONAL VULNERABILITY, DO NOT COPY2contract Victim {3 mapping (address => uint256) public balances;45 function deposit() external payable {6 balances[msg.sender] += msg.value;7 }89 function withdraw() external {10 uint256 amount = balances[msg.sender];11 (bool success, ) = msg.sender.call.value(amount)("");12 require(success);13 balances[msg.sender] = 0;14 }15}16显示全部复制
为了让用户能够归还之前存储在智能合约中的ETH,该函数将
读取用户余额 发送用户余额会将余额重置为 0,因此他们无法再次提取余额。
如果从普通帐户(例如您自己的 MetaMask 帐户)调用,此函数将按预期工作:msg.sender.call.value() 只是将以太币发送到您的帐户。 但是,智能合约也可以调用其他合约。 如果自制的恶意合约调用withdraw(),msg.sender.call.value()不仅会发送一定数量的Ether,还会秘密调用合约开始执行代码。 想象一下这个恶意合约:
1contract 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}13显示全部复制
调用 Attacker.beginAttack() 将启动一个循环来寻找类似的东西:
10.) Attacker's EOA calls Attacker.beginAttack() with 1 ETH20.) Attacker.beginAttack() deposits 1 ETH into Victim34 1.) Attacker -> Victim.withdraw()5 1.) Victim reads balances[msg.sender]6 1.) Victim sends ETH to Attacker (which executes default function)7 2.) Attacker -> Victim.withdraw()8 2.) Victim reads balances[msg.sender]9 2.) Victim sends ETH to Attacker (which executes default function)10 3.) Attacker -> Victim.withdraw()11 3.) Victim reads balances[msg.sender]12 3.) Victim sends ETH to Attacker (which executes default function)13 4.) Attacker no longer has enough gas, returns without calling again14 3.) balances[msg.sender] = 0;15 2.) balances[msg.sender] = 0; (it was already 0)16 1.) balances[msg.sender] = 0; (it was already 0)17显示全部
攻击者账户调用 Attacker.beginAttack 函数并携带 1 个 ETH 会反复攻击受害者账户,并会赚取远远超过其提供的 ETH 数量(这些额外的 ETH 将从其他用户账户的余额中赚取,这将导致受害者的账户余额减少)
如何解决重入攻击(错误的方式)
防止重入攻击的一种简单方法是让您的代码不与任何智能合约交互。 但是当你搜索stackoverflow时,你会发现这个代码片段被很多人使用:
1function isContract(address addr) internal returns (bool) {2 uint size;3 assembly { size := extcodesize(addr) }4 return size > 0;5}6复制
这似乎是有道理的:合约本身拥有一些代码,如果调用者也拥有其他代码,则不能允许合约代码存储数据。 让我们添加它:
1// 该合约存在已知漏洞,请勿复制2contract ContractCheckVictim {3 mapping (address => uint256) public balances;45 function isContract(address addr) internal returns (bool) {6 uint size;7 assembly { size := extcodesize(addr) }8 return size > 0;9 }1011 function deposit() external payable {12 require(!isContract(msg.sender)); // <- 新增行13 balances[msg.sender] += msg.value;14 }1516 function withdraw() external {17 uint256 amount = balances[msg.sender];18 (bool success, ) = msg.sender.call.value(amount)("");19 require(success);20 balances[msg.sender] = 0;21 }22}23显示全部复制
现在为了存入以太币,你的地址不能有智能合约的代码。 但是,可以使用以下攻击者合约轻松击败它:
1contract ContractCheckAttacker {2 constructor() public payable {3 ContractCheckVictim(VICTIM_ADDRESS).deposit(1 ether); // <- New line4 }56 function beginAttack() external payable {7 ContractCheckVictim(VICTIM_ADDRESS).withdraw();8 }910 function() external payable {11 if (gasleft() > 40000) {12 Victim(VICTIM_ADDRESS).withdraw();13 }14 }15}16显示全部复制
之前的攻击是对合约逻辑的攻击,这次是对以太坊合约部署行为的攻击。 在部署过程中,合约还没有返回要部署到其地址的代码,但在这个过程中保留了完整的以太坊虚拟机控制阶段。
从技术上讲以太坊合约地址能否修改,以下代码可用于防止智能合约调用您的代码:
1require(tx.origin == msg.sender)2复制
但是,这仍然不是一个很好的解决方案。 因为以太坊最令人兴奋的方面之一是它的可组合性,智能合约在其中相互集成和发展。 通过使用上面的代码,您限制了项目的用途。
如何解决重入攻击(正确的方法)
通过简单地切换存储更新和外部调用的顺序,我们可以防止启用攻击的重入条件。 尽管可能,回调的回退对攻击者没有好处,因为余额存储已经设置为 0。
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}9复制
上面的代码遵循“check-effect-interact”设计模式,这有助于防止重入攻击。您可以在此处阅读有关 check-validate-interaction 的更多信息
如何解决重入攻击(核心选择)
任何时候您将 ETH 发送到不受信任的地址或与未知合约交互(例如调用 transfer() 到用户提供的令牌地址),您都可以自己打开重入。 通过设计既不发送以太币也不调用不受信任的合约的智能合约,您将防止重入攻击的可能性!
更多攻击类型
上述攻击类型包括智能合约编码问题(可重入)和以太坊怪事(在合约构造函数内部运行代码,合约地址仅被编码)。 还有更多的攻击类型需要注意,例如: