内容简介:不要因为一次攻击,就拒绝使用新技术。本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
不要因为一次攻击,就拒绝使用新技术。
可重入攻击不是ERC777的错
我在去年 9 月写过一篇ERC科普文章: ERC777 功能型代币(通证)最佳实践 ,文章里我推荐新开发的代币使用 ERC777 标准。
Imtoken 使用 ERC777 发行 imbtc 其实是非常值得称赞的,典型的反面是 USDT (transfer不返回值)坑了多少项目。
周末两天Uniswap 和 Lendf.me 都发生了黑客攻击事件,都是Defi 应用与 ERC777 组合应用导致可重入漏洞,其中导致 Lendf.me 损失抵押资产千万美元。
发生这样的事情,相信是所有从业者不愿意看到的,本文也无意针对Lendf.me,你们也是受害者,只是看到有人甩锅给 ERC777 ,不忍从技术角度说几句公道话。 要把锅全甩给 ERC777 ,是特朗普坏(甩锅给你,只因你太优秀)。
ERC777 是一个好的Token标准, 可以极大的提高Defi 应用的用户体验,通过使用的 Hook 回调机制,在 ERC20 中需要二笔或多笔完成的交易(当然还有其他的特性),而使用ERC777单笔交易就可以完成。
对行业的发展我一直是乐观派, 如果因为本次攻击,拒绝使用ERC777,那一定在开历史倒车 。这次事件挫败了大家对 Defi的信心, 从长远看,我相信会让行业更健康。
可重入攻击是怎么发生的?
下面我用一段简洁的代码说明可重入攻击是如何发生的(警告,以下是代码请勿使用),下面是 Defi 应用最常见的逻辑,deposit 函数用来存款,存款时会记录下用户的存款金额,withdraw 函数用来取款,取款在余额的基础上加上一个利率。
interface IToken { function transfer(address recipient, uint256 amount) external returns (bool); function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); } contract Defi { IToken token; mapping(address => uint) balances; function deposit(uint256 amount) external { uint balance = balances[msg.sender] + amount; if(token.transferFrom(msg.sender, this, amount)){ balances[msg.sender] = balance; } } function withdraw() external { if(token.transfer(msg.sender, balances[msg.sender] + 利息)) { // 取回后余额设置为 0 balances[msg.sender] = 0; } } }
在交互过程中,存在 3 个角色,用户、Defi合约、Token合约, 用户存款和取款的时序图是这样的:
sequenceDiagram Note left of 用户: 授权 用户->>Token 合约: Approve(Defi, 100) Note left of 用户: 存款 用户->>DeFi合约: deposit(100) DeFi合约 ->> Token 合约: transferFrom(用户,Defi,100) Note left of 用户: 取款 用户->>DeFi合约: withdraw() DeFi合约 ->> Token 合约: transfer(用户,110)
此时一切运行正常,(经过测试后)用户在一段时间之后可以赎回 110 个 token,开开心心发布上线了。
后来上线了一个 ERC777 代币, ERC777 定义了以下两个hook 接口:
interface ERC777TokensSender { function tokensToSend( address operator, address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData ) external; }
interface ERC777TokensRecipient { function tokensReceived( address operator, address from, address to, uint256 amount, bytes calldata data, bytes calldata operatorData ) external; }
用来同时发送者和接收者进行相应的响应,当然发送者和接收者也可以选择不响应(不实现接口)。
ERC777 的转账实现一般类似下面这样:(transfer 和 transferFrom 实现差不多,下面用transfer举例)
function transfer(address to, uint256 amount) public returns (bool) { if (有发送者接口实现) { 发送者.tokensToSend(operator, from, to, amount, userData, operatorData); } _move(from, from, to, amount, "", ""); if (有接收者接口实现) { 接收者.tokensReceived(operator, from, to, amount, userData, operatorData); } return true; }
简单来说,就是在更改 发送者 和 接收者余额的前后查看是否需要通知发送者和接收者,大部分情况下,普通账号对普通账号的转账(因为普通一般不会实现接口)和 ERC20 效果上一样的。
如果发送者和接收者实现了ERC777的转账接口, 上面的存款调用时序图就是这样的:
sequenceDiagram Note left of 用户: 授权 用户->>Token 合约: Approve(Defi, 100) Note left of 用户: 存款 用户->>DeFi合约: deposit(100) DeFi合约 ->> Token 合约: transferFrom(用户,Defi,100) Token 合约 ->> 用户: tokensToSend() Token 合约 ->> DeFi合约: tokensReceived()
在Defi合约调用Token 的transferFrom 时,Token合约会调用 tokensToSend 和 tokenReceived 以便发送者和接收者进行相应的相应。注意这里tokensToSend 由用户实现,tokenReceived 由 Defi 合约实现。
这个回调能力做很多有趣的事情,比如: 可以把授权和存款合并为一笔交易,用户直接调用 token 合约的转账,Defi 合约收到转账后,在tokenReceived中完成用户的存款操作。
ERC777 协议没有对用户如何实现tokensToSend 及 tokenReceived 做出规定,Defi合约开发者也不应该对参与方的实现进行任何的假定。 在 Lendf.me 的攻击案例中,黑客用户就是在tokensToSend的实现中,调用了 Defi 合约的 withdraw ,黑客用户合约的代码大概是这样的:
contract Hacker { IToken token; IDefi defi; function hack() external { token.approve(defi, 100); defi.deposit(100) } function tokensToSend() external { defi.withdraw() } }
黑客攻击的时序图如下:
sequenceDiagram Hacker->>Hacker合约: hack() Hacker合约->>Token 合约: Approve(Defi, 100) Hacker合约->>DeFi合约: deposit(100) DeFi合约 ->> Token 合约: transferFrom(Hacker合约,Defi,100) Token 合约 ->> Hacker合约: tokensToSend() Hacker合约 ->> DeFi合约: withdraw() Note left of Hacker合约 : 赎回所有存款 Token 合约 ->> DeFi合约: tokensReceived()
注意 tokensToSend() 、 withdraw()和tokensReceived() 函数都是在 transferFrom()中执行的,deposit的代码:
function deposit(uint256 amount) external { uint balance = balances[msg.sender] + amount; if(token.transferFrom(msg.sender, this, amount)){ balances[msg.sender] = balance; } }
根据只要前面 3 个函数没有出错,transferFrom执行成功之后,就重置用户余额(黑客合约)为 100(存款金额)。而实际上黑客已经把所有存款全部取出,从而实现了一次对 Defi 合约的攻击。
大家都没方法控制合约的实现,但是甩锅到 ERC777 对吗? 那么对于 Defi 开发者,如何避免攻击呢?
避免 ERC777 重入攻击
其实可重入攻击一直都存在,OpenZeppelin 也给过解决方案,给 Defi 合约加上重入限制即可。
contract Defi { bool private _notEntered; IToken token; mapping(address => uint) balances; modifier nonReentrant() { require(_notEntered, "ReentrancyGuard: reentrant call"); _notEntered = false; _; _notEntered = true; } function deposit(uint256 amount) external nonReentrant { if(token.transferFrom(msg.sender, this, amount)){ balances[msg.sender] = balances[msg.sender] + amount; } } function withdraw() external nonReentrant { if(token.transfer(msg.sender, balances[msg.sender] + 利息)) { // 取回后余额设置为 0 balances[msg.sender] = 0; } } }
给deposit 和 withdraw 函数加入重入限制后,此时如果在 tokensToSend中调用withdraw就会败而回退交易。很明显在 Defi 合约中可以避免重入攻击。
最后希望 Lendf.me 度过难关。
转载请注明来自登链社区 Tiny 熊
可重入攻击不是ERC777的错
我在去年 9 月写过一篇ERC科普文章: ERC777 功能型代币(通证)最佳实践 ,文章里我推荐新开发的代币使用 ERC777 标准。
Imtoken 使用 ERC777 发行 imbtc 其实是非常值得称赞的,典型的反面是 USDT (transfer不返回值)坑了多少项目。
周末两天Uniswap 和 Lendf.me 都发生了黑客攻击事件,都是Defi 应用与 ERC777 组合应用导致可重入漏洞,其中导致 Lendf.me 损失抵押资产千万美元。
发生这样的事情,相信是所有从业者不愿意看到的,本文也无意针对Lendf.me,你们也是受害者,只是看到有人甩锅给 ERC777 ,不忍从技术角度说几句公道话。 要把锅全甩给 ERC777 ,是特朗普坏(甩锅给你,只因你太优秀)。
ERC777 是一个好的Token标准, 可以极大的提高Defi 应用的用户体验,通过使用的 Hook 回调机制,在 ERC20 中需要二笔或多笔完成的交易(当然还有其他的特性),而使用ERC777单笔交易就可以完成。
对行业的发展我一直是乐观派, 如果因为本次攻击,拒绝使用ERC777,那一定在开历史倒车 。这次事件挫败了大家对 Defi的信心, 从长远看,我相信会让行业更健康。
可重入攻击是怎么发生的?
下面我用一段简洁的代码说明可重入攻击是如何发生的(警告,以下是代码请勿使用),下面是 Defi 应用最常见的逻辑,deposit 函数用来存款,存款时会记录下用户的存款金额,withdraw 函数用来取款,取款在余额的基础上加上一个利率。
interface IToken { function transfer(address recipient, uint256 amount) external returns (bool); function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); } contract Defi { IToken token; mapping(address => uint) balances; function deposit(uint256 amount) external { uint balance = balances[msg.sender] + amount; if(token.transferFrom(msg.sender, this, amount)){ balances[msg.sender] = balance; } } function withdraw() external { if(token.transfer(msg.sender, balances[msg.sender] + 利息)) { // 取回后余额设置为 0 balances[msg.sender] = 0; } } }
在交互过程中,存在 3 个角色,用户、Defi合约、Token合约, 用户存款和取款的时序图是这样的:
sequenceDiagram Note left of 用户: 授权 用户->>Token 合约: Approve(Defi, 100) Note left of 用户: 存款 用户->>DeFi合约: deposit(100) DeFi合约 ->> Token 合约: transferFrom(用户,Defi,100) Note left of 用户: 取款 用户->>DeFi合约: withdraw() DeFi合约 ->> Token 合约: transfer(用户,110)
此时一切运行正常,(经过测试后)用户在一段时间之后可以赎回 110 个 token,开开心心发布上线了。
后来上线了一个 ERC777 代币, ERC777 定义了以下两个hook 接口:
interface ERC777TokensSender { function tokensToSend( address operator, address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData ) external; }
interface ERC777TokensRecipient { function tokensReceived( address operator, address from, address to, uint256 amount, bytes calldata data, bytes calldata operatorData ) external; }
用来同时发送者和接收者进行相应的响应,当然发送者和接收者也可以选择不响应(不实现接口)。
ERC777 的转账实现一般类似下面这样:(transfer 和 transferFrom 实现差不多,下面用transfer举例)
function transfer(address to, uint256 amount) public returns (bool) { if (有发送者接口实现) { 发送者.tokensToSend(operator, from, to, amount, userData, operatorData); } _move(from, from, to, amount, "", ""); if (有接收者接口实现) { 接收者.tokensReceived(operator, from, to, amount, userData, operatorData); } return true; }
简单来说,就是在更改 发送者 和 接收者余额的前后查看是否需要通知发送者和接收者,大部分情况下,普通账号对普通账号的转账(因为普通一般不会实现接口)和 ERC20 效果上一样的。
如果发送者和接收者实现了ERC777的转账接口, 上面的存款调用时序图就是这样的:
sequenceDiagram Note left of 用户: 授权 用户->>Token 合约: Approve(Defi, 100) Note left of 用户: 存款 用户->>DeFi合约: deposit(100) DeFi合约 ->> Token 合约: transferFrom(用户,Defi,100) Token 合约 ->> 用户: tokensToSend() Token 合约 ->> DeFi合约: tokensReceived()
在Defi合约调用Token 的transferFrom 时,Token合约会调用 tokensToSend 和 tokenReceived 以便发送者和接收者进行相应的相应。注意这里tokensToSend 由用户实现,tokenReceived 由 Defi 合约实现。
这个回调能力做很多有趣的事情,比如: 可以把授权和存款合并为一笔交易,用户直接调用 token 合约的转账,Defi 合约收到转账后,在tokenReceived中完成用户的存款操作。
ERC777 协议没有对用户如何实现tokensToSend 及 tokenReceived 做出规定,Defi合约开发者也不应该对参与方的实现进行任何的假定。 在 Lendf.me 的攻击案例中,黑客用户就是在tokensToSend的实现中,调用了 Defi 合约的 withdraw ,黑客用户合约的代码大概是这样的:
contract Hacker { IToken token; IDefi defi; function hack() external { token.approve(defi, 100); defi.deposit(100) } function tokensToSend() external { defi.withdraw() } }
黑客攻击的时序图如下:
sequenceDiagram Hacker->>Hacker合约: hack() Hacker合约->>Token 合约: Approve(Defi, 100) Hacker合约->>DeFi合约: deposit(100) DeFi合约 ->> Token 合约: transferFrom(Hacker合约,Defi,100) Token 合约 ->> Hacker合约: tokensToSend() Hacker合约 ->> DeFi合约: withdraw() Note left of Hacker合约 : 赎回所有存款 Token 合约 ->> DeFi合约: tokensReceived()
注意 tokensToSend() 、 withdraw()和tokensReceived() 函数都是在 transferFrom()中执行的,deposit的代码:
function deposit(uint256 amount) external { uint balance = balances[msg.sender] + amount; if(token.transferFrom(msg.sender, this, amount)){ balances[msg.sender] = balance; } }
根据只要前面 3 个函数没有出错,transferFrom执行成功之后,就重置用户余额(黑客合约)为 100(存款金额)。而实际上黑客已经把所有存款全部取出,从而实现了一次对 Defi 合约的攻击。
大家都没方法控制合约的实现,但是甩锅到 ERC777 对吗? 那么对于 Defi 开发者,如何避免攻击呢?
避免 ERC777 重入攻击
其实可重入攻击一直都存在,OpenZeppelin 也给过解决方案,给 Defi 合约加上重入限制即可。
contract Defi { bool private _notEntered; IToken token; mapping(address => uint) balances; modifier nonReentrant() { require(_notEntered, "ReentrancyGuard: reentrant call"); _notEntered = false; _; _notEntered = true; } function deposit(uint256 amount) external nonReentrant { if(token.transferFrom(msg.sender, this, amount)){ balances[msg.sender] = balances[msg.sender] + amount; } } function withdraw() external nonReentrant { if(token.transfer(msg.sender, balances[msg.sender] + 利息)) { // 取回后余额设置为 0 balances[msg.sender] = 0; } } }
给deposit 和 withdraw 函数加入重入限制后,此时如果在 tokensToSend中调用withdraw就会败而回退交易。很明显在 Defi 合约中可以避免重入攻击。
最后希望 Lendf.me 度过难关。
转载请注明来自登链社区 Tiny 熊
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
- 发表于 20分钟前
- 阅读 ( 92 )
- 学分 ( 0 )
- 分类:以太坊
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 成为前端,你不该学的东西,以及不该做的事儿!
- 到底该不该用 C++ 异常?
- 开源中就不该有软件专利?!
- 抛开性能,谈谈不该用@Synchronized的原因
- 小软件开发该不该实施 GJB5000?
- 中国AI芯片初创公司被美国巨头收购,政府该不该管?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。