# reentrancy-pattern-analysis

Systematically detects all reentrancy vulnerability variants in smart contracts — classic, cross-function, cross-contract, and read-only reentrancy. Builds call graphs, verifies CEI (Checks-Effects-Interactions) pattern compliance, traces state changes relative to external calls, and identifies callback vectors through ERC-777/ERC-1155 hooks. Use when auditing contracts that make external calls, transfer ETH or tokens, interact with callback-enabled standards, or have complex multi-contract architectures.

- **Kind:** skill
- **Source:** https://github.com/quillai-network/qs_skills
- **Page:** https://forefy.com/skills/baa4d633-1277-417c-9812-725442b76ec8
- **API (JSON + files):** https://forefy.com/api/asr/baa4d633-1277-417c-9812-725442b76ec8

---

## SKILL.md

---
name: reentrancy-pattern-analysis
description: Systematically detects all reentrancy vulnerability variants in smart contracts — classic, cross-function, cross-contract, and read-only reentrancy. Builds call graphs, verifies CEI (Checks-Effects-Interactions) pattern compliance, traces state changes relative to external calls, and identifies callback vectors through ERC-777/ERC-1155 hooks. Use when auditing contracts that make external calls, transfer ETH or tokens, interact with callback-enabled standards, or have complex multi-contract architectures.
---

# Reentrancy Pattern Analysis

Systematically detect **all variants** of reentrancy vulnerabilities by mapping the relationship between external calls and state changes across the entire contract system.

## When to Use

- Auditing any contract that makes external calls (ETH transfers, token interactions, cross-contract calls)
- Reviewing contracts integrating with callback-enabled token standards (ERC-777, ERC-1155)
- Analyzing DeFi protocols with multi-contract architectures
- Verifying reentrancy guard coverage across all entry points
- When traditional tools only check for classic reentrancy but miss cross-function or read-only variants

## When NOT to Use

- Pure state variable analysis without external calls (use state-invariant-detection)
- Access control consistency checking (use semantic-guard-analysis)
- Full multi-dimensional audit (use behavioral-state-analysis, which orchestrates this skill)

## Core Concept: The CEI Invariant

**Checks-Effects-Interactions (CEI)** is the fundamental safety pattern:

```
1. CHECKS   — Validate all conditions (require statements, access control)
2. EFFECTS  — Update all state variables
3. INTERACTIONS — Make external calls (ETH transfers, token calls, cross-contract)
```

**Any function that performs INTERACTIONS before completing all EFFECTS is potentially vulnerable to reentrancy.**

## The Five Reentrancy Variants

### Variant 1: Classic Single-Function Reentrancy

The original and most well-known pattern. A function makes an external call before updating its own state, allowing the callee to re-enter the same function.

```solidity
// VULNERABLE
function withdraw(uint256 amount) public {
    require(balances[msg.sender] >= amount);
    (bool success, ) = msg.sender.call{value: amount}(""); // INTERACTION before EFFECT
    require(success);
    balances[msg.sender] -= amount; // State update AFTER external call
}
```

**Detection**: Find functions where state writes to variables used in `require` checks occur AFTER external calls.

### Variant 2: Cross-Function Reentrancy

Two or more functions share state, and an attacker re-enters through a DIFFERENT function than the one making the external call.

```solidity
function withdraw(uint256 amount) public {
    require(balances[msg.sender] >= amount);
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
    balances[msg.sender] -= amount;
}

// Attacker re-enters HERE during withdraw's external call
function transfer(address to, uint256 amount) public {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;
    balances[to] += amount;
}
```

**Detection**: For each external call in function F, check if any OTHER public function reads/writes the same state variables that F modifies after the call.

### Variant 3: Cross-Contract Reentrancy

The re-entry occurs through a different contract that shares state or trust relationships with the vulnerable contract.

```solidity
// Contract A
function withdrawFromVault() public {
    uint256 shares = vault.balanceOf(msg.sender);
    vault.burn(msg.sender, shares);
    // External call — attacker can re-enter Contract B
    (bool success, ) = msg.sender.call{value: shares * pricePerShare}("");
    require(success);
}

// Contract B (attacker re-enters here)
function borrow() public {
    uint256 collateral = vault.balanceOf(msg.sender); // Reads stale state!
    // Shares not yet burned, so collateral appears inflated
    uint256 loanAmount = collateral * maxLTV;
    token.transfer(msg.sender, loanAmount);
}
```

**Detection**: Map all cross-contract dependencies. For each external call, identify which other contracts read the state that should have been updated.

### Variant 4: Read-Only Reentrancy

A view/pure function returns stale state during a reentrancy callback. No state is modified during re-entry — the attacker exploits the READING of inconsistent state by a third-party contract.

```solidity
// Pool contract
function removeLiquidity() external {
    uint256 shares = balances[msg.sender];
    // Burns LP tokens (updates internal accounting)
    _burn(msg.sender, shares);
    // External call BEFORE updating reserves
    (bool success, ) = msg.sender.call{value: ethAmount}("");
    // Reserves updated AFTER the call
    totalReserves -= ethAmount;
}

// This view function returns stale data during the callback
function getRate() public view returns (uint256) {
    return totalReserves / totalSupply(); // totalReserves not yet updated!
}

// Third-party contract reads the inflated rate
function priceOracle() external view returns (uint256) {
    return pool.getRate(); // Returns wrong value during reentrancy
}
```

**Detection**: For each external call, identify view functions that read state variables modified AFTER the call. Check if any external protocol depends on those view functions.

### Variant 5: ERC-777 / ERC-1155 Callback Reentrancy

Token standards with built-in callback hooks that execute arbitrary code on the receiver during transfers.

```solidity
// ERC-777: tokensReceived() hook called on recipient
// ERC-1155: onERC1155Received() hook called on recipient
// ERC-721: onERC721Received() hook called on recipient

function deposit(uint256 amount) public {
    token.transferFrom(msg.sender, address(this), amount); // Triggers callback!
    // If token is ERC-777, msg.sender's tokensReceived() runs HERE
    balances[msg.sender] += amount; // State update after callback
}
```

**Detection**: Identify all token `transfer`/`transferFrom`/`safeTransfer` calls. Check if the token could be ERC-777/ERC-1155/ERC-721. Verify state updates happen before the transfer.

## Three-Phase Detection Architecture

### Phase 1: Call Graph Construction

Build a complete map of all external interactions.

**For each function, extract:**

```
Function: withdraw()
├── External Calls:
│   ├── msg.sender.call{value: amount}("") at line 45
│   ├── token.transfer(user, amount) at line 48
│   └── oracle.getPrice() at line 42
├── State Writes:
│   ├── balances[msg.sender] -= amount at line 50
│   └── totalWithdrawn += amount at line 51
├── State Reads (in requires):
│   └── balances[msg.sender] at line 41
└── Modifiers:
    └── nonReentrant: NO
```

**Call Classification:**

| Call Type | Reentrancy Risk | Examples |
|-----------|----------------|---------|
| ETH transfer via `call` | HIGH | `addr.call{value: x}("")` |
| Token `transfer`/`transferFrom` | MEDIUM-HIGH | ERC-777 hooks, ERC-1155 callbacks |
| `safeTransferFrom` (NFT) | MEDIUM | ERC-721 `onERC721Received` callback |
| Cross-contract function call | MEDIUM | `otherContract.doSomething()` |
| `staticcall` / view calls | LOW | Cannot modify state but can trigger read-only reentrancy in callers |
| `delegatecall` | HIGH | Executes in caller's context |

### Phase 2: CEI Violation Detection

For each function with external calls, verify CEI ordering.

**Algorithm:**

```
For each function F with external calls:
  1. E = set of all state variables written by F
  2. C = set of all state variables read in require/if checks
  3. I = position of each external call in F
  4. For each external call at position P:
     a. W_after = state writes that occur AFTER position P
     b. If W_after ∩ (E ∪ C) ≠ ∅:
        → CEI VIOLATION: state modified after external call
     c. Classify violation:
        - W_after ∩ C ≠ ∅ → Classic reentrancy (check variable modified after call)
        - W_after ∩ E ≠ ∅ → State inconsistency window
```

**Cross-Function Extension:**

```
For each external call in function F at position P:
  W_before = state variables NOT yet updated at position P
  For each OTHER public function G:
    R_G = state variables read by G
    W_G = state variables written by G
    If R_G ∩ W_before ≠ ∅ OR W_G ∩ W_before ≠ ∅:
      → CROSS-FUNCTION REENTRANCY: G can be called during F's external call
         with inconsistent state
```

### Phase 3: Guard Coverage Verification

Check that reentrancy protections are correctly applied.

**Guard Types:**

| Guard | Coverage | Limitations |
|-------|----------|-------------|
| `nonReentrant` modifier (OpenZeppelin) | Single contract, all functions with modifier | Does not protect cross-contract reentrancy |
| CEI pattern compliance | Per-function | Must be verified for every function individually |
| `transfer()` / `send()` (2300 gas) | Limits callback gas | NOT safe — EIP-1884 changed gas costs; don't rely on this |
| Pull payment pattern | Eliminates external calls from state changes | Requires architectural change |

**Verification:**

```
For each function F with CEI violations:
  1. Check if F has nonReentrant modifier → Mitigated (single-contract only)
  2. Check if ALL functions sharing state also have nonReentrant → Mitigated (cross-function)
  3. Check if cross-contract consumers are protected → Requires manual review
  4. If no guard → VULNERABLE
```

## Workflow

```
Task Progress:
- [ ] Step 1: Identify all external calls in every function (ETH transfers, token calls, cross-contract)
- [ ] Step 2: Build call graph with state read/write positions relative to each call
- [ ] Step 3: Detect CEI violations (state writes after external calls)
- [ ] Step 4: Detect cross-function reentrancy (shared state across functions)
- [ ] Step 5: Detect callback vectors (ERC-777, ERC-1155, ERC-721 token interactions)
- [ ] Step 6: Detect read-only reentrancy (view functions reading stale state)
- [ ] Step 7: Verify guard coverage (nonReentrant, CEI compliance, pull patterns)
- [ ] Step 8: Score findings and generate report
```

## Output Format

```markdown
## Reentrancy Analysis Report

### Finding: [Title]

**Function:** `functionName()` at `Contract.sol:L42`
**Variant:** [Classic | Cross-Function | Cross-Contract | Read-Only | Callback]
**Severity:** [CRITICAL | HIGH | MEDIUM]
**Guard Status:** [Unguarded | Partially Guarded | Guarded]

**CEI Violation:**
  - External call at line [X]: `[call expression]`
  - State write AFTER call at line [Y]: `[state variable] = [expression]`

**Re-Entry Path:**
  1. Attacker calls `functionName()`
  2. External call triggers callback to attacker contract
  3. Attacker re-enters via `[re-entry function]`
  4. State variable `[name]` still has pre-update value
  5. [Exploit consequence]

**Impact:**
[Funds drained, state corrupted, price manipulated, etc.]

**Recommendation:**
[Specific fix — reorder state updates, add nonReentrant, use pull pattern]
```

## Severity Classification

| Variant | State Modified | Funds at Risk | Severity |
|---------|---------------|---------------|----------|
| Classic — ETH drain | Yes | Yes | **CRITICAL** |
| Cross-function — balance manipulation | Yes | Yes | **CRITICAL** |
| Cross-contract — oracle/price manipulation | Indirectly | Yes | **HIGH** |
| Read-only — stale price in third-party | No (view only) | Possibly | **HIGH** |
| Callback — ERC-777 deposit inflation | Yes | Possibly | **HIGH** |
| Any variant with nonReentrant on target | Mitigated | No | **LOW/INFO** |

## Advanced Detection: Transitive Reentrancy

Trace reentrancy through multiple contract hops:

```
Contract A calls Contract B
Contract B calls Contract C
Contract C calls back to Contract A (or reads A's stale state)

Detection: Build transitive call graph across all contracts in scope.
For each call chain A → B → ... → X:
  If X can call back to any contract in the chain → TRANSITIVE REENTRANCY
```

## Quick Detection Checklist

When analyzing a contract, immediately check:

- [ ] Does any function make an external call (ETH transfer, token transfer, cross-contract) BEFORE completing all state updates?
- [ ] Are there multiple public functions that modify the same state variables, where at least one makes an external call?
- [ ] Does the contract interact with ERC-777, ERC-1155, or ERC-721 tokens (callback hooks)?
- [ ] Do view functions read state that is only partially updated during an external call?
- [ ] Is `nonReentrant` applied to ALL functions that share state with a function making external calls, not just the calling function itself?
- [ ] Does the contract rely on `transfer()` or `send()` for reentrancy protection? (Unsafe assumption)

For detailed variant taxonomy, see [{baseDir}/references/reentrancy-variants.md]({baseDir}/references/reentrancy-variants.md).
For real-world case studies, see [{baseDir}/references/case-studies.md]({baseDir}/references/case-studies.md).

## Rationalizations to Reject

- "We use `transfer()` so reentrancy is impossible" → EIP-1884 changed gas costs; `transfer` is no longer considered safe
- "The function has `nonReentrant`" → Check cross-function and cross-contract paths; one modifier doesn't protect everything
- "It's just a view function" → Read-only reentrancy can manipulate prices and oracles in third-party contracts
- "We only interact with standard ERC20 tokens" → ERC-777 is backward-compatible with ERC20; token type may change
- "The external call is to a trusted contract" → Trust boundaries shift; verify the actual code path through all intermediaries
- "State is updated right after the call" → "Right after" is too late; the call already happened

## references

```

```

## references/case-studies.md

# Reentrancy Case Studies

## Case Study 1: The DAO Hack (2016) — Classic Reentrancy

### Overview

- **Loss:** $60 million (3.6M ETH)
- **Variant:** Classic single-function reentrancy
- **Root Cause:** ETH sent before balance updated in `splitDAO()`

### Vulnerable Pattern

```solidity
function splitDAO(uint _proposalID, address _newCurator) {
    // ... checks ...

    uint fundsToBeMoved = (balances[msg.sender] * p.splitData[0].totalSupply) /
                           p.splitData[0].totalSupply;

    // INTERACTION before EFFECT
    if (!p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender)) {
        throw;
    }

    // EFFECT after INTERACTION — too late
    balances[msg.sender] = 0;
}
```

### Attack Sequence

```
1. Attacker creates a proposal to split DAO
2. Calls splitDAO() → sends ETH to attacker contract
3. Attacker's fallback() re-enters splitDAO()
4. balances[attacker] still > 0 (not yet zeroed)
5. Repeats until contract drained
6. Stack unwinds, balances[attacker] = 0 (only once)
```

### Detection

```
State variable: balances[msg.sender]
External call: createTokenProxy.value(fundsToBeMoved)(msg.sender)
State write: balances[msg.sender] = 0

Call position: BEFORE state write
→ CLASSIC REENTRANCY DETECTED
```

### Lesson

This hack led to the Ethereum/Ethereum Classic fork and the creation of the CEI pattern as a fundamental security principle.

---

## Case Study 2: Curve Pool Read-Only Reentrancy (2023)

### Overview

- **Loss:** ~$70 million across multiple protocols
- **Variant:** Read-only reentrancy
- **Root Cause:** Vyper compiler bug in reentrancy locks + stale `get_virtual_price()` during callback

### Vulnerable Pattern

```python
# Vyper (Curve pool)
@external
def remove_liquidity(amount: uint256):
    # Burns LP tokens (updates totalSupply)
    self._burn(msg.sender, amount)

    # Sends ETH — callback opportunity
    raw_call(msg.sender, b"", value=eth_amount)

    # Updates reserves AFTER the call
    self.balances[0] -= eth_amount

@view
@external
def get_virtual_price() -> uint256:
    # During callback: totalSupply decreased, balances NOT yet
    # Returns INFLATED price
    return self._get_virtual_price()
```

### Attack Sequence

```
1. Attacker calls remove_liquidity() on Curve pool
2. LP tokens burned (totalSupply decreases)
3. ETH sent to attacker → triggers fallback
4. In fallback, attacker calls a lending protocol
5. Lending protocol calls pool.get_virtual_price() for collateral pricing
6. get_virtual_price() returns inflated value (reserves not yet decreased)
7. Attacker borrows against inflated collateral
8. remove_liquidity() completes, reserves decrease
9. Attacker's collateral now worth less than borrowed amount
```

### Detection

```
Function: remove_liquidity()
State update after external call: self.balances[0] -= eth_amount
View function reading stale state: get_virtual_price() reads self.balances

→ READ-ONLY REENTRANCY WINDOW in get_virtual_price()
→ Any protocol using get_virtual_price() for pricing is vulnerable
```

### Lesson

Read-only reentrancy is invisible to traditional tools that only check for state modifications during re-entry. The vulnerability exists in the VIEW function, not the state-modifying function.

---

## Case Study 3: Fei Protocol / Rari Capital (2022)

### Overview

- **Loss:** $80 million
- **Variant:** Classic reentrancy in a Compound fork
- **Root Cause:** Missing reentrancy protection on borrow function with CEI violation

### Vulnerable Pattern

```solidity
// Compound-style cToken
function borrow(uint256 amount) external {
    // Check
    require(getAccountLiquidity(msg.sender) >= amount);

    // Interaction BEFORE Effect
    underlying.transfer(msg.sender, amount); // External call

    // Effect AFTER Interaction
    accountBorrows[msg.sender] += amount;
    totalBorrows += amount;
}
```

### Attack Sequence

```
1. Attacker deposits collateral
2. Calls borrow() on vulnerable cToken
3. Token transfer triggers attacker's callback
4. accountBorrows not yet updated — liquidity check passes again
5. Attacker calls borrow() again (and again)
6. Each call passes liquidity check because borrows not recorded
7. Drains pool far beyond collateral value
```

### Detection

```
Function: borrow()
External call: underlying.transfer(msg.sender, amount) at position P
State writes after P:
  - accountBorrows[msg.sender] += amount
  - totalBorrows += amount

Both state writes are read in getAccountLiquidity() require check
→ CLASSIC REENTRANCY: check variable updated after external call
```

---

## Case Study 4: Cream Finance via ERC-777 (2021)

### Overview

- **Loss:** $18.8 million
- **Variant:** ERC-777 callback reentrancy
- **Root Cause:** AMP token (ERC-777 compatible) triggered `tokensReceived` hook during supply

### Vulnerable Pattern

```solidity
function borrow(uint256 amount) external {
    require(getAccountLiquidity(msg.sender) > 0);

    // AMP token is ERC-777 — triggers tokensReceived on recipient
    ampToken.transfer(msg.sender, amount);

    // State update after ERC-777 callback
    accountBorrows[msg.sender] += amount;
}
```

### Attack Sequence

```
1. Attacker deposits ETH as collateral
2. Calls borrow() for AMP tokens
3. AMP.transfer() triggers tokensReceived() on attacker
4. In tokensReceived(), attacker calls borrow() AGAIN
5. accountBorrows not yet updated — liquidity still shows positive
6. Second borrow also succeeds
7. Repeat until pool drained
```

### Detection

```
Token interaction: ampToken.transfer(msg.sender, amount)
Token type: ERC-777 (has tokensReceived hook)
State write after transfer: accountBorrows[msg.sender] += amount

→ ERC-777 CALLBACK REENTRANCY
→ Severity: CRITICAL (funds at risk)
```

### Lesson

Any protocol that integrates with ERC-777 tokens must treat `transfer()` as an external call with callback potential. ERC-777 is backward-compatible with ERC-20, so a token that appears to be ERC-20 may actually have callback hooks.

---

## Case Study 5: Lendf.Me / imBTC (2020)

### Overview

- **Loss:** $25 million
- **Variant:** ERC-777 callback reentrancy on `supply()`
- **Root Cause:** imBTC (ERC-777) used in Compound fork without reentrancy protection

### Attack Flow

```
1. Attacker supplies imBTC (ERC-777 token) as collateral
2. transferFrom() triggers tokensToSend() hook on sender
3. In callback, attacker withdraws their supply
4. Original supply() continues, crediting attacker again
5. Result: Double-counted collateral → borrow against phantom collateral
```

### Detection Pattern

```
Token: imBTC (ERC-777)
Function: supply() calls token.transferFrom()
ERC-777 hook: tokensToSend() on sender during transferFrom()
State write after transferFrom(): accountSupply[user] += amount

→ CALLBACK REENTRANCY via ERC-777 tokensToSend hook
```

---

## Summary: Detection Patterns Across Cases

| Case | Year | Loss | Variant | Key Signal |
|------|------|------|---------|------------|
| The DAO | 2016 | $60M | Classic | Balance zeroed after ETH send |
| Lendf.Me | 2020 | $25M | ERC-777 callback | transferFrom before supply accounting |
| Cream Finance | 2021 | $18.8M | ERC-777 callback | ERC-777 transfer before borrow accounting |
| Fei/Rari | 2022 | $80M | Classic | Token transfer before borrow accounting |
| Curve pools | 2023 | $70M | Read-only | View function returns stale reserves |

**Common pattern across ALL cases:** State update occurs AFTER an external call that can trigger attacker-controlled code.

## references/reentrancy-variants.md

# Reentrancy Variant Taxonomy — Detailed Reference

## Variant 1: Classic Single-Function Reentrancy

### Pattern

```
Function F:
  1. Read state S
  2. Check condition on S
  3. Make external call (INTERACTION)
  4. Update state S (EFFECT after INTERACTION — violation)
```

### Vulnerable Code Example

```solidity
function withdraw() public {
    uint256 bal = balances[msg.sender];
    require(bal > 0, "No balance");

    // INTERACTION — external call BEFORE state update
    (bool success, ) = msg.sender.call{value: bal}("");
    require(success);

    // EFFECT — too late, attacker already re-entered
    balances[msg.sender] = 0;
}
```

### Fixed Code

```solidity
function withdraw() public {
    uint256 bal = balances[msg.sender];
    require(bal > 0, "No balance");

    // EFFECT — update state FIRST
    balances[msg.sender] = 0;

    // INTERACTION — external call AFTER state update
    (bool success, ) = msg.sender.call{value: bal}("");
    require(success);
}
```

### Detection Heuristic

```
For function F:
  If ∃ external_call at position P AND ∃ state_write at position Q
  WHERE Q > P AND state_write.variable ∈ F.require_variables
  → CLASSIC REENTRANCY
```

---

## Variant 2: Cross-Function Reentrancy

### Pattern

```
Function F (makes external call):
  1. Read state S
  2. Make external call → attacker callback
  3. Update state S

Function G (re-entry target):
  1. Read SAME state S (still has pre-update value)
  2. Perform action based on stale S
```

### Vulnerable Code Example

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

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount);
        // External call — attacker re-enters transfer()
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
        balances[msg.sender] -= amount;
    }

    function transfer(address to, uint256 amount) public {
        // During reentrancy, balances[msg.sender] is NOT yet decremented
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}
```

### Attack Sequence

```
1. Attacker has 10 ETH balance
2. Calls withdraw(10 ETH)
3. Contract sends 10 ETH → triggers attacker's receive()
4. In receive(), attacker calls transfer(accomplice, 10 ETH)
   → balances[attacker] is still 10 (not yet decremented)
   → Transfer succeeds
5. withdraw() resumes: balances[attacker] -= 10
   → balances[attacker] = 0 (but accomplice already has 10)
6. Result: 10 ETH withdrawn + 10 ETH transferred = 20 ETH extracted from 10 ETH deposit
```

### Detection Heuristic

```
For function F with external_call at position P:
  S_pending = state variables written AFTER P in F
  For each OTHER public function G:
    If G reads or writes any variable in S_pending:
      → CROSS-FUNCTION REENTRANCY between F and G
```

### Key Insight

`nonReentrant` on `withdraw()` alone is NOT sufficient. The modifier must ALSO be on `transfer()` (or any function sharing state).

---

## Variant 3: Cross-Contract Reentrancy

### Pattern

```
Contract A:
  1. Updates partial state
  2. Makes external call to user/contract
  3. Updates remaining state

Contract B (depends on Contract A):
  1. Reads Contract A's state (partially updated)
  2. Makes decisions based on stale/inconsistent data
```

### Vulnerable Code Example

```solidity
// Lending Protocol
contract LendingPool {
    mapping(address => uint256) public collateral;

    function withdrawCollateral(uint256 amount) public {
        require(collateral[msg.sender] >= amount);
        collateral[msg.sender] -= amount;

        // External call — attacker callback here
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);

        // No more state to update — but damage is elsewhere
    }
}

// Separate borrowing contract that reads LendingPool state
contract BorrowingModule {
    LendingPool pool;

    function borrow(uint256 amount) public {
        // Reads collateral — but during reentrancy from withdrawCollateral,
        // collateral IS already decremented (CEI followed in LendingPool)
        // ... unless the architecture has other stale dependencies
        uint256 col = pool.collateral(msg.sender);
        require(col * LTV >= amount, "Undercollateralized");
        // Issue: What if there's a different state dependency?
    }
}
```

### Real Cross-Contract Pattern

The more dangerous pattern involves contracts that cache or snapshot state:

```solidity
contract VaultShares {
    function getSharePrice() public view returns (uint256) {
        return totalAssets / totalShares; // Read during callback = stale
    }

    function withdraw(uint256 shares) external {
        uint256 assets = shares * getSharePrice();
        _burn(msg.sender, shares);
        // totalShares updated, but totalAssets NOT yet
        asset.transfer(msg.sender, assets); // Callback opportunity
        totalAssets -= assets; // Updated AFTER transfer
    }
}

contract LendingMarket {
    VaultShares vault;

    function liquidate(address user) external {
        uint256 collateralValue = vault.balanceOf(user) * vault.getSharePrice();
        // During reentrancy: getSharePrice() returns inflated value
        // because totalAssets not yet decremented
    }
}
```

### Detection Heuristic

```
For each contract C in scope:
  For each external call in C at position P:
    S_stale = state variables not yet updated at P
    For each OTHER contract D that reads C's state:
      If D reads any variable in S_stale (directly or via view functions):
        → CROSS-CONTRACT REENTRANCY: D sees inconsistent state from C
```

---

## Variant 4: Read-Only Reentrancy

### Pattern

```
Contract A:
  1. Updates some state
  2. Makes external call → attacker callback
  3. Updates remaining state

Contract A's view function:
  - Returns value based on partially-updated state
  - This value is WRONG during the callback window

Contract B (victim):
  - Calls Contract A's view function during the callback
  - Makes financial decisions based on wrong value
```

### Key Insight

No state is modified during re-entry. The attack purely exploits **reading inconsistent state** from a view function. `nonReentrant` on the view function would break legitimate callers.

### Vulnerable Code Example (Curve/Vyper Style)

```solidity
contract StablePool {
    uint256 public totalReserves;
    uint256 public totalLPTokens;

    function removeLiquidity(uint256 lpAmount) external {
        uint256 ethAmount = lpAmount * totalReserves / totalLPTokens;

        // Effect: burn LP tokens
        totalLPTokens -= lpAmount;

        // Interaction: send ETH (callback opportunity)
        (bool success, ) = msg.sender.call{value: ethAmount}("");
        require(success);

        // Effect: update reserves AFTER call
        totalReserves -= ethAmount;
    }

    // This view function returns WRONG value during the callback
    function getVirtualPrice() public view returns (uint256) {
        return totalReserves / totalLPTokens;
        // During callback: totalLPTokens decreased, totalReserves NOT yet
        // → Virtual price is INFLATED
    }
}

// Victim protocol
contract LendingProtocol {
    StablePool pool;

    function getCollateralValue(address user) public view returns (uint256) {
        return userLPBalance[user] * pool.getVirtualPrice();
        // Returns inflated value during reentrancy window
    }

    function borrow(uint256 amount) external {
        require(getCollateralValue(msg.sender) >= amount * RATIO);
        // Attacker borrows against inflated collateral value
        token.transfer(msg.sender, amount);
    }
}
```

### Detection Heuristic

```
For each function F with external call at position P:
  S_post = state variables updated AFTER P
  For each view/pure function V in the same contract:
    If V reads any variable in S_post:
      → READ-ONLY REENTRANCY WINDOW: V returns stale value during F's callback
  Flag severity based on:
    - Does any external protocol depend on V?
    - Is V used for pricing, collateral valuation, or access control?
```

---

## Variant 5: Token Callback Reentrancy

### ERC-777 `tokensReceived` Hook

```solidity
// ERC-777 automatically calls tokensReceived() on the recipient
contract Vulnerable {
    function deposit(uint256 amount) public {
        // This triggers tokensReceived() on msg.sender if token is ERC-777
        token.transferFrom(msg.sender, address(this), amount);
        // State update AFTER the callback
        balances[msg.sender] += amount;
    }
}

// Attacker contract
contract Attacker is IERC777Recipient {
    function tokensReceived(...) external override {
        // Re-enter deposit() or any other function
        // balances[attacker] not yet updated
        vulnerable.withdraw(previousBalance);
    }
}
```

### ERC-1155 `onERC1155Received` Hook

```solidity
contract NFTMarket {
    function buyNFT(uint256 tokenId) public payable {
        // safeTransferFrom triggers onERC1155Received on recipient
        nft.safeTransferFrom(seller, msg.sender, tokenId, 1, "");
        // State update after callback
        listings[tokenId].active = false;
    }
}
```

### ERC-721 `onERC721Received` Hook

```solidity
contract NFTStaking {
    function stake(uint256 tokenId) public {
        // safeTransferFrom triggers onERC721Received
        nft.safeTransferFrom(msg.sender, address(this), tokenId);
        stakedBy[tokenId] = msg.sender;
        totalStaked += 1;
    }
}
```

### Detection Heuristic

```
For each token interaction:
  If function calls:
    - ERC777.send() or ERC777.transfer() → tokensReceived callback
    - ERC1155.safeTransferFrom() → onERC1155Received callback
    - ERC721.safeTransferFrom() → onERC721Received callback
    - ERC721.safeMint() → onERC721Received callback
  Check if state updates occur AFTER the token call
  → TOKEN CALLBACK REENTRANCY
```

---

## Guard Effectiveness Matrix

| Guard | Classic | Cross-Function | Cross-Contract | Read-Only | Callback |
|-------|---------|----------------|----------------|-----------|----------|
| `nonReentrant` on calling function | YES | NO (unless on ALL shared functions) | NO | NO | YES |
| `nonReentrant` on ALL public functions | YES | YES | NO | NO | YES |
| CEI pattern compliance | YES | YES | PARTIAL | NO | YES |
| Pull payment pattern | YES | YES | YES | NO | YES |
| `transfer()`/`send()` (2300 gas) | UNRELIABLE | UNRELIABLE | NO | NO | NO |
| OpenZeppelin ReentrancyGuard (global) | YES | YES | NO | NO | YES |

### Recommendation Priority

1. **Always follow CEI pattern** — prevents most variants
2. **Apply `nonReentrant` to all state-modifying functions** — catches cross-function
3. **Audit view functions for stale state during callbacks** — catches read-only
4. **Map cross-contract dependencies** — catches cross-contract
5. **Never rely on gas limits for safety** — EIP changes can break this assumption

