什么是重入攻击?
在理解重入攻击前,首先要了解 Fallback 函数。 Fallback 函数是智能合约里的一个特殊函数,没有参数,也返回值。比如,如果A合约调用B合约中的函数中含有转账行为方法,就会调用默认A函数中的fallback函数。
了解了 Fallback 函数的特性,就可以很容易的解释重入攻击的逻辑。简单的讲,重入攻击有点类似传统项目的高并发问题。
举个例子,现在有一个整点秒杀的场景,秒杀N个商品,到点之后,大量用户涌进,发起抢购请求。后台服务的操作逻辑可以简单的分这么几步:
发起抢购请求 => 校对库存 => 减少库存 => 用户购买成功修改用户余额
整个操作必须是原子性的,在整个逻辑未进行完之前有必要给关键的数据库上锁,加上事务。如果其中某一步出现问题,比如某人连续发起高频请求,也就是重入攻击,可能后台在进行到减少库存尚未成功修改用户余额时,后台再次执行另外一次秒杀逻辑,就会造成问题。
在合约调用的过程中,利用智能合约的 Fallback 函数特性,如果再加上不恰当的编写(生效逻辑放在最后),就会给攻击者可乘之机。例如:
// 攻击合约
import './buy.sol';
contract Attacker {
uint public count;
// 初始化被攻击合约
buy buyContract;
function AttachInit(address buyAddress) {
buyContract = buy(buyAddress);
}
// 攻击 buy 合约
function startAttack() {
buyContract.buyGoods();
}
// fallback函数
function () payable {
// 执行十次重入
if (count < 5) {
buyContract.buyGoods();
}
}
// 受攻击合约
contract buy {
function buyGoods() payable external {
uint256 amount = balanceOf[msg.sender];
// 在转账成功后,会触发发送者合约的 Fallback 函数
(bool success, ) = msg.sender.call.value(amount)("");
require(success, "transfer fail.");
balanceOf[msg.sender] = 0;
}
}
如何解决?
在智能合约层级,好的解决办法就是在逻辑开始前,首先将 用户的余额修改,之后再进行转账逻辑
contract buy {
function buyGoods() payable external {
uint256 amount = balanceOf[msg.sender];
balanceOf[msg.sender] = 0;
// 在转账成功后,会触发发送者合约的 Fallback 函数
(bool success, ) = msg.sender.call.value(amount)("");
require(success, "transfer fail.");
}
}
还有一个简单的方法,就是加锁。现在已经有成熟的防治重入攻击的库来进行使用了。就没必要自己造这个小轮子了,文档地址。
使用方法:
命令行:
npm install @openzeppelin/contracts
合约代码:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"
contract buy is ReentrancyGuard {
function buyGoods() external payable whenActive nonReentrant {
uint256 amount = balanceOf[msg.sender];
(bool success, ) = msg.sender.call.value(amount)("");
require(success, "transfer fail.");
balanceOf[msg.sender] = 0;
}
}
有兴趣可以自己去库里看看 ReentrancyGuard 源码的逻辑,也是很简单的。