Skip to main content
ClaudeWave
Skill55 repo starsupdated 4mo ago

reentrancy

>

Install in Claude Code
Copy
git clone --depth 1 https://github.com/PurpleAILAB/Vigilo /tmp/reentrancy && cp -r /tmp/reentrancy/packages/opencode/skills/reentrancy ~/.claude/skills/reentrancy
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# Reentrancy Vulnerability Analysis

**OWASP SC05:2025** - Reentrancy ranks **#5** in OWASP Smart Contract Top 10 (2025). Historical losses exceed **$500M+**, with 2024-2025 attacks including Penpie ($27M), Clober, GemPad, and multiple ERC721/1155 callback exploits.

---

## Why Reentrancy Happens (Root Causes)

### Root Cause 1: State Update After External Call

The fundamental CEI (Checks-Effects-Interactions) violation.

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

**Attacker's view**: "Between the call and the state update, I control execution. I'll call withdraw again before my balance updates."

### Root Cause 2: Execution Flow Transfer

Every external call hands control to potentially hostile code.

```solidity
// Even "safe" patterns can transfer control:
IERC20(token).transfer(recipient, amount);    // Could be ERC777 with hooks!
NFT.safeTransferFrom(from, to, id);           // Triggers onERC721Received!
Token1155.safeTransferFrom(...);              // Triggers onERC1155Received!
```

**Detection**: Any external call is a potential callback. Check what standards the token implements.

### Root Cause 3: Shared State Dependency

Multiple contracts/functions depend on same state variable.

```solidity
// Contract has two functions sharing `balances`
function withdraw() external {
    uint256 amount = balances[msg.sender];
    msg.sender.call{value: amount}("");  // @audit Callback opportunity
    balances[msg.sender] = 0;  // @audit Updated after
}

function transfer(address to, uint256 amount) external {
    require(balances[msg.sender] >= amount);  // @audit Same balance!
    balances[msg.sender] -= amount;
    balances[to] += amount;
}
// During withdraw callback, attacker calls transfer() with STALE balance!
```

**Attacker's view**: "ReentrancyGuard on withdraw() doesn't protect transfer(). I'll call transfer() during withdraw callback."

### Root Cause 4: View Function Exposure

View functions return stale data during callbacks, affecting external protocols.

```solidity
// Contract A (Vault)
function withdraw() external {
    uint256 assets = userAssets[msg.sender];
    msg.sender.call{value: assets}("");  // @audit Callback
    userAssets[msg.sender] = 0;
    totalAssets -= assets;  // @audit Updated after
}

function getTotalAssets() public view returns (uint256) {
    return totalAssets;  // @audit Returns STALE value during callback!
}

// Contract B (Lending) reads from Contract A
function getCollateralValue(address user) view returns (uint256) {
    return vaultA.getTotalAssets() * userShares / totalShares;
    // During withdraw callback, totalAssets is WRONG!
}
```

**Attacker's view**: "The view function shows the old value. External protocols that read this will make wrong decisions."

---

## The State Timeline (Core Artifact)

Every reentrancy finding MUST include a state timeline:

```
T0: balances[attacker] = 100, contract.balance = 1000
T1: withdraw(100) called
T2: call{value: 100}("") → attacker.receive() triggered
T3: [CALLBACK] balances[attacker] STILL = 100! ← INCONSISTENT STATE
T4: Re-enter withdraw(100) with same balance
T5: Another 100 sent
T6: ... repeat until drained
T7: balances[attacker] -= 100 (executed N times, but all see 100)
```

Document each finding:

| Phase | Contract State | Attacker Action | Balance Check |
|-------|---------------|-----------------|---------------|
| T0 | Initial | - | 100 |
| T2 | Sending ETH | receive() callback | 100 (stale) |
| T4 | Re-entered | withdraw again | 100 (stale) |
| T7 | Unwind | - | Multiple decrements fail |

---

## Detection Patterns

### Pattern 1: Classic CEI Violation

**Root Cause**: State Update After External Call

```solidity
// VULNERABLE: Textbook reentrancy
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount);  // CHECK
    (bool success,) = msg.sender.call{value: amount}("");  // INTERACTION
    require(success);
    balances[msg.sender] -= amount;  // EFFECT - Wrong order!
}
```

**Attack Flow**:
1. Attacker calls withdraw(100)
2. Contract sends ETH via call{value}
3. Attacker's receive() callback triggers
4. In callback: call withdraw(100) again (balance still 100!)
5. Repeat until contract drained
6. All state updates execute with wrong values

**Search Queries**:
```
Grep("\\.call\\{value", glob="**/*.sol")
Grep("transfer\\(|send\\(", glob="**/*.sol")
```

**Verification Questions**:
- Is state updated BEFORE external call?
- Is there a reentrancy guard?
- Can callback reach this function again?

### Pattern 2: Cross-Function Reentrancy

**Root Cause**: Shared State Dependency

```solidity
// Both functions use same `balances` mapping
function withdraw() external nonReentrant {  // Has guard
    uint256 amount = balances[msg.sender];
    msg.sender.call{value: amount}("");  // @audit Callback
    balances[msg.sender] = 0;
}

function transfer(address to, uint256 amt) external {  // NO guard!
    require(balances[msg.sender] >= amt);  // @audit Same state!
    balances[msg.sender] -= amt;
    balances[to] += amt;
}
```

**Attack Flow**:
1. Attacker calls withdraw() with 100 balance
2. During callback, attacker calls transfer(accomplice, 100)
3. transfer() sees balances[attacker] = 100 (not yet updated!)
4. Attacker "transfers" 100 to accomplice
5. withdraw() completes, sets balances[attacker] = 0
6. Accomplice has 100 tokens created from nothing

**Search Queries**:
```
Grep("nonReentrant", glob="**/*.sol")
Grep("ReentrancyGuard", glob="**/*.sol")
```

**Verification Questions**:
- Does guard cover ALL functions sharing this state?
- Can other functions be called during callback?
- What state do they depend on?

### Pattern 3: Cross-Contract Reentrancy

**Root Cause**: Ree