Skip to main content

reentrancy-patterns

Comprehensive reentrancy attack patterns and defenses for Solidity. Use when writing contracts with external calls, integrating tokens with callbacks (ERC-777, ERC-1363), or auditing for reentrancy vulnerabilities. Covers all variants including read-only reentrancy.

Jump to install

Source facts

Repository
ccashwell/evm-cortex
Last source activity
April 10, 2026 at 16:31
Detected SKILL.md language
English
Stars
131
Forks
18

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
reentrancy-patterns
description
Comprehensive reentrancy attack patterns and defenses for Solidity. Use when writing contracts with external calls, integrating tokens with callbacks (ERC-777, ERC-1363), or auditing for reentrancy vulnerabilities. Covers all variants including read-only reentrancy.
# Reentrancy Patterns ## Single-Function Reentrancy The classic variant. An external call re-enters the same function before state is updated. ```solidity // VULNERABLE: state update after external call contract VulnerableVault { mapping(address => uint256) public balances; function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount); (bool success,) = msg.sender.call{value: amount}(""); // ← re-entry point require(success); balances[msg.sender] -= amount; // ← too late, attacker already re-entered } } // FIXED: checks-effects-interactions contract FixedVault { mapping(address => uint256) public balances; function withdraw(uint256 amount) external { if (balances[msg.sender] < amount) revert InsufficientBalance(); balances[msg.sender] -= amount; // effect BEFORE interaction (bool success,) = msg.sender.call{value: amount}(""); if (!success) revert TransferFailed(); } } ``` ## Cross-Function Reentrancy Attacker re-enters a **different** function that reads stale state. ```solidity // VULNERABLE: withdraw and transfer share state contract Vulnerable { mapping(address => uint256) public balances; function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount); (bool success,) = msg.sender.call{value: amount}(""); // ← attacker calls transfer() here require(success); balances[msg.sender] -= amount; } function transfer(address to, uint256 amount) external { require(balances[msg.sender] >= amount); balances[msg.sender] -= amount; // uses stale balance balances[to] += amount; } } ``` **Defense**: ReentrancyGuard on all functions sharing state, plus CEI ordering. ## Cross-Contract Reentrancy Attacker re-enters a different contract that reads stale state from the first. ```solidity // Contract A: Lending pool that updates user balance // Contract B: Reads user balance from A for collateral check // Attack: Borrow from A → callback during withdrawal → B reads stale (higher) balance → over-borrow // Defense: ReentrancyGuard + cross-contract state synchronization // OR: use a protocol-wide reentrancy lock ``` ### Protocol-Wide Reentrancy Lock ```solidity contract ReentrancyLock { uint256 private constant _NOT_ENTERED = 1; uint256 private constant _ENTERED = 2; uint256 private _status = _NOT_ENTERED; error ReentrancyGuard_ReentrantCall(); modifier globalNonReentrant() { if (_status == _ENTERED) revert ReentrancyGuard_ReentrantCall(); _status = _ENTERED; _; _status = _NOT_ENTERED; } } // All contracts in the protocol inherit from or check the same lock contract LendingPool is ReentrancyLock { function borrow(uint256 amount) external globalNonReentrant { ... } } contract CollateralManager is ReentrancyLock { function checkCollateral(address user) external globalNonReentrant { ... } } ``` ## Read-Only Reentrancy The most subtle variant. A view function returns stale state during a callback, and a **different protocol** reads that stale state. ```solidity // Scenario: Curve pool with removeLiquidity that triggers a callback // 1. Attacker calls removeLiquidity() on Curve pool // 2. Curve sends ETH to attacker (callback) // 3. During callback, pool's internal accounting is stale // 4. Attacker calls a lending protocol that reads Curve's get_virtual_price() // 5. Virtual price is inflated (stale state) → attacker borrows more than they should // Defense in the reading protocol: function getCollateralValue(address pool) internal view returns (uint256) { // Check if the Curve pool is mid-operation // Many Curve pools have a reentrancy lock that can be checked try ICurvePool(pool).claim_admin_fees() { // If this succeeds, pool is not locked — safe to read } catch { revert PoolReentrant(); } return ICurvePool(pool).get_virtual_price(); } ``` **Key insight**: Read-only reentrancy doesn't attack the contract making the callback. It attacks **other contracts** that read the first contract's stale view functions. ## ERC-777 Callback Reentrancy ERC-777 tokens call `tokensReceived()` on the recipient, creating a reentrancy vector on every transfer. ```solidity // VULNERABLE: ERC-777 token triggers callback on transfer function deposit(uint256 amount) external { shares[msg.sender] += calculateShares(amount); IERC777(token).send(address(this), amount, ""); // callback before state finalized } // SAFE: use ReentrancyGuard + CEI function deposit(uint256 amount) external nonReentrant { uint256 shareAmount = calculateShares(amount); shares[msg.sender] += shareAmount; IERC20(token).safeTransferFrom(msg.sender, address(this), amount); emit Deposited(msg.sender, amount, shareAmount); } ``` ## ERC-1363 Callback Reentrancy Similar to ERC-777 — `onTransferReceived()` is called on the recipient. ```solidity // Any token implementing ERC-1363 transferAndCall will trigger onTransferReceived // Always use ReentrancyGuard when accepting arbitrary tokens ``` ## OpenZeppelin ReentrancyGuard ```solidity import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; contract Vault is ReentrancyGuard { // Apply to ALL functions that: // 1. Make external calls // 2. Share state with functions that make external calls // 3. Are called by other protocols that might read your state function deposit(uint256 amount) external nonReentrant { // safe from reentrancy } function withdraw(uint256 amount) external nonReentrant { // safe from reentrancy } // View functions don't need nonReentrant unless read-only reentrancy is a concern // In that case, add a reentrancy check without the mutex reset: function getSharePrice() external view returns (uint256) { // Consider if stale reads during reentrancy could be exploited } } ``` ## Transient Storage Reentrancy Lock (EIP-1153) Post-Cancun, use transient storage for cheaper reentrancy locks (~100 gas vs ~2,900 gas). ```solidity contract TransientReentrancyGuard { bytes32 constant REENTRANCY_SLOT = keccak256("reentrancy.lock"); modifier nonReentrant() { assembly { if tload(REENTRANCY_SLOT) { revert(0, 0) } tstore(REENTRANCY_SLOT, 1) } _; assembly { tstore(REENTRANCY_SLOT, 0) } } } ``` ## Reentrancy Checklist - [ ] All functions with external calls have `nonReentrant` - [ ] CEI ordering followed even with ReentrancyGuard (defense in depth) - [ ] Cross-function reentrancy: all functions sharing state are guarded - [ ] Cross-contract reentrancy: protocol-wide lock if contracts share state - [ ] Read-only reentrancy: view functions considered for stale state exposure - [ ] ERC-777/ERC-1363 tokens: callbacks accounted for in deposit/transfer flows - [ ] Transient storage lock considered for post-Cancun deployments
View on GitHub