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.

설치로 이동

소스 정보

저장소
ccashwell/evm-cortex
최근 소스 활동
2026년 4월 10일 16:31
감지된 SKILL.md 언어
영어
스타
131
포크
18

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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
GitHub에서 보기