solidity 中如何解决重入攻击问题


什么是重入攻击?

在理解重入攻击前,首先要了解 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 源码的逻辑,也是很简单的。