Use when writing Solidity smart contracts that transfer ETH or call external contracts — applying the Checks-Effects-Interactions pattern and ReentrancyGuard to prevent reentrancy attacks.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add jeffreytse/grimoire-core --skill prevent-reentrancy --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Prevent Reentrancy?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jeffreytse-prevent-reentrancy)More formats (shields.io, HTML) on the badges page.
---
name: prevent-reentrancy
description: Use when writing Solidity smart contracts that transfer ETH or call external contracts — applying the Checks-Effects-Interactions pattern and ReentrancyGuard to prevent reentrancy attacks.
source: 'OWASP Smart Contract Top 10 SC05 (owasp.org/www-project-smart-contract-top-10/); SWC-107 (Smart Contract Weakness Classification); OpenZeppelin ReentrancyGuard documentation; Ethereum Foundation security documentation'
tags: [security, owasp, solidity, smart-contracts, reentrancy, ethereum, blockchain, developer]
---
# Prevent Reentrancy
Apply Checks-Effects-Interactions (CEI) pattern and OpenZeppelin's ReentrancyGuard to prevent reentrancy attacks — the attack vector behind the $60M DAO hack and dozens of subsequent DeFi exploits.
## Why This Is Best Practice
**Adopted by:** OWASP Smart Contract Top 10 SC05 (Reentrancy Attacks). SWC-107 (Smart Contract Weakness Classification Registry) is the authoritative vulnerability taxonomy. OpenZeppelin's Contracts library (used in 70%+ of DeFi protocols including Uniswap, Aave, Compound) provides `ReentrancyGuard` as the standard defense. The Ethereum Foundation's Smart Contract Security Best Practices mandate CEI pattern for all external calls.
**Impact:** The 2016 DAO hack exploited reentrancy to drain 3.6M ETH (~$60M at the time, ~$11B at 2021 peak prices), triggering the Ethereum hard fork. Since 2020: Cream Finance lost $18.8M (2021), Fei Protocol lost $80M (2022), and Euler Finance lost $197M (2023) — all reentrancy variants. DeFi reentrancy losses exceed $1B according to Rekt.news (2023). A single missing `nonReentrant` modifier causes complete fund drainage in protocols with external calls.
**Why best:** CEI pattern (validate, then update state, then call external) ensures that when a malicious contract re-enters the function, the state is already updated and the re-entry path fails the checks. The alternative (optimistic pattern: call first, update state after) allows repeated withdrawals before balance is decremented — the exact DAO vulnerability. ReentrancyGuard adds a mutex that catches cross-function reentrancy which CEI alone doesn't prevent.
Sources: OWASP Smart Contract Top 10 SC05; SWC-107; OpenZeppelin ReentrancyGuard source; Ethereum Foundation Smart Contract Best Practices
## Steps
1. **Apply Checks-Effects-Interactions pattern — update state before external calls**:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// BAD — vulnerable to reentrancy
contract VulnerableVault {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// INTERACTION before EFFECT — reentrancy window here
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] = 0; // too late — state updated after external call
}
}
// GOOD — CEI pattern
contract SecureVault {
mapping(address => uint256) public balances;
function withdraw() external {
// CHECK
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// EFFECT — update state before external call
balances[msg.sender] = 0;
// INTERACTION — external call last
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
```
2. **Use OpenZeppelin ReentrancyGuard for all external-calling functions**:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract DeFiProtocol is ReentrancyGuard {
mapping(address => uint256) public deposits;
IERC20 public token;
function deposit(uint256 amount) external nonReentrant {
token.transferFrom(msg.sender, address(this), amount);
deposits[msg.sender] += amount;
}
function withdraw(uint256 amount) external nonReentrant {
require(deposits[msg.sender] >= amount, "Insufficient balance");
deposits[msg.sender] -= amount; // EFFECT before INTERACTION
token.transfer(msg.sender, amount);
}
}
```
3. **Protect cross-function reentrancy — same mutex across related functions**:
```solidity
// Cross-function reentrancy: attacker calls withdrawETH → enters depositETH during callback
contract CrossFunctionSafe is ReentrancyGuard {
mapping(address => uint256) public ethBalance;
mapping(address => uint256) public tokenBalance;
// Both functions protected by same mutex
function withdrawETH(uint256 amount) external nonReentrant {
require(ethBalance[msg.sender] >= amount);
ethBalance[msg.sender] -= amount;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
}
function depositToken(uint256 amount) external nonReentrant {
// If called during withdrawETH callback, mutex blocks this
tokenBalance[msg.sender] += amount;
}
}
```
4. **Use pull-over-push payment pattern for ETH distribution**:
```solidity
// GOOD — pull pattern: recipient claims funds instead of contract pushing
contract PullPayment {
mapping(address => uint256) private _pendingWithdrawals;
function _asyncTransfer(address recipient, uint256 amount) internal {
_pendingWithdrawals[recipient] += amount;
}
function withdrawPayments() external nonReentrant {
uint256 payment = _pendingWithdrawals[msg.sender];
require(payment > 0, "No pending payment");
_pendingWithdrawals[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: payment}("");
require(ok, "Transfer failed");
}
}
```
5. **Test reentrancy attacks in your test suite**:
```solidity
// Test helper: malicious contract that re-enters on receive
contract ReentrancyAttacker {
IVault public target;
uint256 public attackCount;
receive() external payable {
if (attackCount < 5 && address(target).balance >= 1 ether) {
attackCount++;
target.withdraw(); // re-enter
}
}
function attack() external payable {
target.deposit{value: msg.value}();
target.withdraw();
}
}
```
```javascript
// Hardhat test
it("should prevent reentrancy", async function () {
const attacker = await ReentrancyAttacker.deploy(vault.address);
await attacker.attack({ value: ethers.parseEther("1") });
expect(await attacker.attackCount()).to.equal(0); // reentrancy blocked
});
```
## Rules
- CEI order is: Checks (require/revert), then Effects (state changes), then Interactions (external calls/transfers) — never call external contracts before updating state.
- `transfer()` and `send()` have a 2300 gas stipend that prevents most reentrancy — but this is not reliable post-EIP-1884 and should not replace CEI or ReentrancyGuard.
- Read-only reentrancy (attacker reads state during callback to manipulate price oracle) is not prevented by ReentrancyGuard — also apply view function guards on price-sensitive reads.
- Every function that interacts with an external contract or transfers ETH must have `nonReentrant`.
## Common Mistakes
- **CEI without ReentrancyGuard on multi-function contracts** — CEI prevents same-function reentrancy; cross-function reentrancy requires the mutex.
- **Using `transfer()` and believing it's safe** — the 2300 gas stipend is not guaranteed in all contexts (proxies, `receive()` hooks with state writes).
- **Not testing with a malicious contract** — unit tests with EOA (externally owned accounts) don't trigger reentrancy; test with a contract that implements `receive()` with re-entry logic.
- **Forgetting reentrancy in ERC777/ERC1155 token hooks** — `_beforeTokenTransfer` and `tokensReceived` hooks execute external code during transfers, creating reentrancy windows even without explicit `.call()`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!