# 防止重入攻击4种方法

### CEI模式

CEI模式是防止重入的简单有效的方法。检查是指条件的真实性。效果是指交互产生的状态修改。最后，交互是指函数或合约之间的事务。

下面是存在漏洞的代码

```js
// contract_A: holds user's funds
function withdraw() external {
​ ​ ​​uint userBalance = userBalances[msg.sender];
​ ​ ​​require(userBalance > 0);
​ ​ ​​(bool success,) = msg.sender.call{ ​​value: ​userBalance ​​}("");
​ ​ ​​require(success,);
​ ​ ​​userBalances[msg.sender] = 0;
}
```

下面是攻击者的接收函数：

```js
// contract_B: reentrancy attackreceive() external payable {  
​ ​ ​​if (address(contract_A).balance >= msg.value) {  
​ ​ ​​​ ​ ​​contract_A.withdraw();  
​ ​ ​​}  
}
```

这个是个很明显的重入，可以将合约中的资金全部掏空。如果改成CEI模式的话就是这样。

```js
function withdraw() external {
​ ​ ​​uint userBalance = userBalances[msg.sender];
​ ​ ​​require(userBalance > 0);
​​​ ​ ​​userBalances[msg.sender] = 0;
​ ​ ​​(bool success,) = msg.sender.call{ ​​value: ​userBalance ​​}("");
​ ​ ​​require(success,);
}
```

精髓就是在转账之前。就已经把userBalances\[msg.sender]的值置为了0。再次进入这个函数的时候require就会报错。实现防重入。

### 互斥锁

互斥锁可以构造为函数或函数修饰符，但逻辑很简单：在易受重入的函数调用周围放置一个布尔锁。“已锁定”的初始状态为 false（已解锁），但在易受攻击的函数执行开始之前立即设置为 true（已锁定），然后在终止后立即设置回 false（已解锁）。

修复上面的函数可以写成这样

```js
bool internal locked = false;
function withdraw() external {
​ ​ ​​require(!locked);
​ ​ ​​locked = true;
​ ​ ​​uint userBalance = userBalances[msg.sender];
 ​ ​​require(userBalance > 0);
​ ​ ​​(bool success,) = msg.sender.call{ ​​value: ​userBalance ​​}("");
​ ​ ​​require(success,);
​ ​ ​​userBalances[msg.sender] = 0;
​ ​ ​​locked = false;
}
```

### 中间商转发(Pull payment)

这个名字的英文是`Pull Payment`。思路就是支付的合约不直接和收款人交互。\
可以参考这个[安全性 - OpenZeppelin Docs](https://docs.openzeppelin.com/contracts/4.x/api/security#PullPayment)

第一步，合约会将资金先发送给`escrow`。

```js
function sendPayment(address user, address escrow) external {
​ ​ ​​require(msg.sender == authorized);
​ ​ ​​uint userBalance = userBalances[user];
​ ​ ​​require(userBalance > 0);
​ ​ ​​userBalances[user] = 0;
​ ​ ​​(bool success,) = escrow.call{ ​​value: ​userBalance ​​}("");
​ ​ ​​require(success,);
}
```

第二步，收款人需要自己提取资金

```js
function pullPayment() external {
​ ​ ​​require(msg.sender == receiver);
​ ​ ​​uint payment = account(this).balance;
​ ​ ​​(bool success,) = msg.sender.call{ ​​value: payment​ ​​}("");
​ ​ ​​require(success,);
}
```

### Gas Limit

这个方法依靠的是transfer或send函数。这两个函数都有2300个单位gas的限制。transfer交易失败会revert。但是send不会。

```js
// transfer will revert if the transaction fails
address(receiver).transfer(amount);
// send will not revert if the transaction fails
address(receiver).send(amount);
```

参考:[Solidity Smart Contract Security: 4 Ways to Prevent Reentrancy Attacks | by insurgent | May, 2022 | Better Programming](https://betterprogramming.pub/solidity-smart-contract-security-preventing-reentrancy-attacks-fc729339a3ff?gi=f68bbaa6cf0b)
