From grimoire
Applies the Checks-Effects-Interactions pattern and OpenZeppelin's ReentrancyGuard to prevent reentrancy attacks in Solidity smart contracts that transfer ETH or call external contracts.
How this skill is triggered — by the user, by Claude, or both
Slash command
/grimoire:prevent-reentrancyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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.
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
Apply Checks-Effects-Interactions pattern — update state before external calls:
// 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");
}
}
Use OpenZeppelin ReentrancyGuard for all external-calling functions:
// 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);
}
}
Protect cross-function reentrancy — same mutex across related functions:
// 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;
}
}
Use pull-over-push payment pattern for ETH distribution:
// 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");
}
}
Test reentrancy attacks in your test suite:
// 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();
}
}
// 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
});
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.nonReentrant.transfer() and believing it's safe — the 2300 gas stipend is not guaranteed in all contexts (proxies, receive() hooks with state writes).receive() with re-entry logic._beforeTokenTransfer and tokensReceived hooks execute external code during transfers, creating reentrancy windows even without explicit .call().npx claudepluginhub jeffreytse/grimoire --plugin grimoirePrevents Solidity smart contract vulnerabilities like reentrancy, overflows, and access control using secure patterns, Checks-Effects-Interactions, and ReentrancyGuard. For writing and auditing contracts.
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.
Provides Solidity security patterns for reentrancy, token decimals, precision loss, with defensive code examples and pre-deploy audit checklist. Use before deploying or reviewing value-handling contracts.