防止重入攻击4种方法

CEI模式

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

下面是存在漏洞的代码

// 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;
}

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

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

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

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(已解锁)。

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

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

第一步,合约会将资金先发送给escrow

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,);
}

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

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不会。

// 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

Last updated