From reentrancy-pattern-analysis
Detects reentrancy vulnerabilities in smart contracts including classic, cross-function, cross-contract, and read-only variants. Verifies CEI pattern, builds call graphs, traces state changes around external calls.
npx claudepluginhub quillai-network/quillshield_skills --plugin reentrancy-pattern-analysisThis skill uses the workspace's default tool permissions.
Systematically detect **all variants** of reentrancy vulnerabilities by mapping the relationship between external calls and state changes across the entire contract system.
Audits smart contracts via Behavioral State Analysis: classifies type, runs targeted threat engines (ETE/ACTE/SITE), tiered output for security reviews and DeFi threat modeling.
Detects logic vulnerabilities in smart contracts by analyzing guard-state consistency patterns. Identifies functions bypassing security checks like require statements and modifiers that others consistently apply. Use for auditing missing access controls or forgotten checks.
Audits EVM and Solana smart contracts for vulnerabilities like reentrancy, access control, oracle manipulation, flash loans, and economic exploits.
Share bugs, ideas, or general feedback.
Systematically detect all variants of reentrancy vulnerabilities by mapping the relationship between external calls and state changes across the entire contract system.
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 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.
// 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.
Two or more functions share state, and an attacker re-enters through a DIFFERENT function than the one making the external call.
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.
The re-entry occurs through a different contract that shares state or trust relationships with the vulnerable contract.
// 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.
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.
// 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.
Token standards with built-in callback hooks that execute arbitrary code on the receiver during transfers.
// 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.
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 |
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
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
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
## 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]
| 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 |
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
When analyzing a contract, immediately check:
nonReentrant applied to ALL functions that share state with a function making external calls, not just the calling function itself?transfer() or send() for reentrancy protection? (Unsafe assumption)For detailed variant taxonomy, see {baseDir}/references/reentrancy-variants.md. For real-world case studies, see {baseDir}/references/case-studies.md.
transfer() so reentrancy is impossible" → EIP-1884 changed gas costs; transfer is no longer considered safenonReentrant" → Check cross-function and cross-contract paths; one modifier doesn't protect everything