name: vuln-web3-reentrancy description: "Scan for reentrancy vulnerabilities (classic, cross-function, cross-contract, read-only, unchecked calls, delegatecall). Appends to vulnerabilities.md." allowed-tools: Read Bash(find *) Bash(grep *) Bash(head *) Bash(wc *) Bash(cat *) Bash(ls *) Write argument-hint: <path to threat-model.md, defaults to ./assessment/threat-model.md>
Bug Bounty — Step 3n: Reentrancy & Unchecked External Calls
Scan for unsafe external call patterns — reentrancy attacks and unchecked call return values.
Input
$ARGUMENTS
- Read
./assessment/threat-model.md(or provided path) for priority targets - Read
./assessment/recon.mdfor entry points and data flows - If either is missing, tell the user which step to run first
Vulnerability Patterns
Vyper-Specific Reentrancy
- Vyper
@nonreentrantdecorator with key-based locking (different keys = different locks) - Vyper compiler bug (0.3.7-0.3.9):
@nonreentrantlock not applied correctly on cross-function calls with same key - Vyper
raw_callwithoutmax_outsizereturning empty bytes (silent failure) - Vyper
send()not checking return value (pre-0.4.0) - Vyper default function (
__default__) acting as fallback — reentrancy via ETH receive - Missing
@nonreentranton functions that share state with guarded functions - Vyper
create_from_blueprint/create_minimal_proxy_towith callback hooks
Grep patterns: @nonreentrant, raw_call, send(, __default__, @external, @internal, create_from_blueprint, create_minimal_proxy_to, .vy
Vyper compiler version check:
# Check for vulnerable Vyper versions (0.3.7-0.3.9 reentrancy lock bug)
grep -r "# @version" --include="*.vy" | grep -E "0\.3\.[789]"
grep -r "pragma version" --include="*.vy" | grep -E "0\.3\.[789]"
Classic Reentrancy
- External calls before state updates (checks-effects-interactions violation)
- ETH transfer via
.call{value:}before balance deduction - Token transfer before internal accounting update
Grep patterns: .call{value:, .call{, withdraw, balances[, msg.sender.call, payable(msg.sender)
Cross-Function Reentrancy
- Shared state modified by multiple functions, one callable during callback
- Function A calls external contract, function B reads stale state during callback
- Mutex not applied across all functions sharing state
Grep patterns: nonReentrant, ReentrancyGuard, _status, locked, modifier
Cross-Contract Reentrancy
- Contract A calls Contract B, which calls back into Contract A via Contract C
- Shared state across multiple contracts not protected by single mutex
- Composability exploits in multi-contract protocols
Grep patterns: interface, external, IPool, IVault, callback, hook, onFlashLoan, uniswapV3SwapCallback
Read-Only Reentrancy
- View functions returning stale state during external call execution
- Price/share calculations based on mid-transaction balances
- Other protocols reading manipulated state during callback
Grep patterns: view, getPrice, getRate, exchangeRate, totalAssets, convertToShares, convertToAssets, balanceOf(address(this))
Unchecked External Calls
- Low-level
.call()without checkingbool successreturn .send()return value ignored (fails silently on 2300 gas)- Token
transfer()/transferFrom()on non-reverting ERC-20s (returns false) - Missing
safeTransfer/safeTransferFromwrapper
Grep patterns: .call(, .send(, (bool success, require(success, safeTransfer, SafeERC20, IERC20(, .transfer(
Delegatecall Injection
delegatecallto user-controlled address- Implementation contract with unprotected
delegatecall delegatecallin loop to untrusted targets
Grep patterns: .delegatecall(, delegatecall, implementation, _delegate(, fallback()
Process
- Find all external calls —
.call,.send,.transfer,delegatecall, token transfers - Check CEI pattern — are state changes made BEFORE the external call?
- Check return values — is every call's success/failure handled?
- Check reentrancy guards — is
nonReentrantapplied to all state-changing functions with external calls? - Trace cross-contract flows — can a callback reach stale state in this or another contract?
- Assess impact — fund drainage, double-spend, state corruption
Output
Append to ./assessment/vulnerabilities.md:
# Vulnerability Findings — Reentrancy & Unchecked Calls
**Date**: {date}
**Scanner**: vuln-web3-reentrancy
## Findings
### VULN-REENT-001: {Title}
**Severity**: {Critical/High/Medium/Low}
**Confidence**: {High/Medium/Low}
**Category**: {Classic Reentrancy / Cross-Function / Cross-Contract / Read-Only / Unchecked Call / Delegatecall}
**Location**: `{file}:{line}`
**CWE**: CWE-{841|252}
**Description**:
{What the vulnerability is}
**Vulnerable Code**:
```solidity
{code snippet showing CEI violation or unchecked call}
`` `
**Attack Scenario**:
1. {Step-by-step: attacker contract calls → callback → re-enters → drains}
**Proof of Concept**:
```solidity
{Attacker contract}
`` `
**Impact**:
{Fund drainage amount, state corruption}
**Remediation**:
```solidity
{Fixed code with CEI pattern or ReentrancyGuard}
`` `
---
Positive Observations
While scanning, note any strong security patterns relevant to this scanner's domain. Add them to the # Positive Security Observations section at the end of vulnerabilities.md:
- {scanner-name}: {what the codebase does well in this area}
Rules
- Verify the reentrancy is exploitable — a callback must exist AND state must be stale at that point.
- Check for ReentrancyGuard — if applied correctly, the issue is mitigated.
- For unchecked calls, confirm the token is non-reverting — standard OZ ERC-20 reverts on failure.
- Idempotent output — if
vulnerabilities.mdalready has a# Vulnerability Findings — Reentrancy & Unchecked Callssection, replace it entirely. Seesc3-vuln-scanidempotency rule. - Save to
./assessment/vulnerabilities.mdand confirm.