Reentrancy can happen if a smart contract calls a second one in a direct way or indirectly by transferring it ether. Typically at the code execution before the given statement terminates it called once again by another contract. Consider the following code fragments:
contract ReentrancyError {
...
function withdraw(uint _amount) public{
if(balances[msg.sender] >= _amount) {
if(msg.sender.call.value(_amount)()) {
balances[msg.sender] -= _amount;
}
}
}
}
contract Attacker {
function attack() public {
target.withdraw(donated);
}
function() public payable{
if (counter < 10) {
counter++;
target.withdraw(donated);
}
}
}
The withdraw function of the ReentrancyError contract is meant to be called by externally owned accounts, however a tricky hacker might force to call the function from a hacked contract. If so by calling the withdraw function at the msg.sender.call.value(_amount) by transferring ether to the Attacker contract the fallback function will be called, that calls the withdraw function back again. As balance of the sender is still not modified at this stage, the attacker will succeed to transfer the amount of ether once again starting a new cycle (up to 10).
Possible solutions in this example are:
- reducing the balance before the ether transfer
- using mutex
- using require statement instead of if