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.

Aller à l'installation

Informations de source

Dépôt
ccashwell/evm-cortex
Dernière activité de la source
10 avril 2026 à 16:31
Langue détectée de SKILL.md
anglais
Étoiles
131
Forks
18

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
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
Voir sur GitHub