CEI模式
CEI模式是防止重入的简单有效的方法。检查是指条件的真实性。效果是指交互产生的状态修改。最后,交互是指函数或合约之间的事务。
下面是存在漏洞的代码
Copy // 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 ;
}
下面是攻击者的接收函数:
Copy // contract_B: reentrancy attackreceive() external payable {
if ( address (contract_A).balance >= msg .value) {
contract_A .withdraw ();
}
}
这个是个很明显的重入,可以将合约中的资金全部掏空。如果改成CEI模式的话就是这样。
Copy 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(已解锁)。
修复上面的函数可以写成这样
Copy 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
。
Copy 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 , );
}
第二步,收款人需要自己提取资金
Copy 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不会。
Copy // 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