DEFI LIBRARY FOUNDATIONAL CONCEPTS

Understanding Reentrancy in DeFi Libraries

9 min read
#Smart Contracts #DeFi Security #Security Audits #Reentrancy #Library Patterns
Understanding Reentrancy in DeFi Libraries

When I first stumbled across a blockchain article about reentrancy, I thought it sounded like some kind of fancy cooking technique. Of course, I didn’t really have a chef’s hat, but the term felt oddly exotic—almost like a recipe for a financial disaster. It turned out that “reentrancy” is a type of vulnerability that can turn a perfectly good smart‑contract library into a gold‑rush trap.

The feeling that keeps me up at night

I think about a small savings account that, instead of earning interest, sometimes gives you back the same money you deposited, multiple times, because the system keeps calling the same function before it finishes. That kind of loop feels creepy. The market has always pushed us to chase quick gains, but I’ve learned that the most harmful crashes are the ones that happen when people forget to lock their own doors.

What reentrancy actually is

At its core, reentrancy means a function within a contract can be invoked again before the first invocation has finished. Imagine you tell someone to hand over a key, and while they’re still handing it over you slip an identical key into the same hands another time. In Solidity the pattern looks like this:

function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount);
    payable(msg.sender).transfer(amount);
    balances[msg.sender] -= amount;
}

If the receiving address is a contract, the transfer call triggers that contract’s fallback function. If that contract then calls withdraw again, it will still find the original balance (because the deduction happens after the transfer). The second call succeeds until the first one finally subtracts the balance.

In a more colloquial sense, the first call is still “in progress,” and the second call runs “inside” it, so the internal state is not yet updated. The vulnerability is called reentrancy because the function re‑enters itself (through an external call) before the first execution has finished.

Why it matters for DeFi

DeFi libraries—those reusable sets of contracts we import into our own projects—often rely on common patterns. A popular example is the open‑source Aave lending pool or the Compound protocol, each with deposit and withdrawal functions exposed to users.

If a developer uses a library that hasn’t been hardened against reentrancy, then a malicious actor can orchestrate a “reentrancy attack” and drain funds. The attackers do not need to break the cryptographic signature; they simply craft a smart contract that takes advantage of the order in which reverts and state changes happen.

The DAO as a historical case

In June 2016 the DAO (Decentralised Autonomous Organisation) lost roughly 3.6 million Ether, which at the time was about $150 million. At the heart of the hack was a subtle reentrancy issue in the DAO’s splitDAO function. Attackers repeatedly called the function before the state update could complete, siphoning more Ether than they owned.

This shows that even a project that started with solid code can end up exposed if the architecture allows a function that “re‑enters” itself via an external call.

What libraries do to guard against it

Over the years the DeFi ecosystem has become much more defensive. Libraries now typically adopt the checks‑effects‑interactions pattern, which rearranges the steps in a function to avoid the vulnerability:

  1. Checks – confirm the inputs and that the user is allowed to perform the action.
  2. Effects – update the contract’s internal state (balances, counts, etc.).
  3. Interactions – make the external call (i.e., send Ether).

When you reorder a function like this:

function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;      // Effect
    payable(msg.sender).transfer(amount); // Interaction
}

now, if the receiving contract does a reentrant call, the balances mapping has already been decreased. The nested call will fail the require check and be reverted.

Other protective patterns are:

  • Reentrancy guard – a mutex that blocks a function from being called twice in the same transaction chain. The OpenZeppelin ReentrancyGuard contract is a popular implementation. It uses a simple bool flag that you set at the start of a function and clear at the end, rolling back on any recursive calls.
  • Pull over push – instead of sending funds immediately, the contract records an outstanding withdrawal that the user can claim by calling a separate withdraw() function. This means each withdrawal is separate from deposits, preventing an attacker from exploiting the same function twice in the same transaction.
  • Use of safeTransfer – a version of the ERC20 transfer that checks for success and reverts on failure. This protects against token contracts that do not return a boolean value.

When reviewing a library, I look for these patterns tucked into the code. Even if a library’s README does not highlight them, the presence of a ReentrancyGuard import or an updateBalance() function that takes effect before interaction is a good sign.

A practical walkthrough: building a basic pool

Let’s walk through a miniature pool contract that uses the checks‑effects‑interactions pattern to safely handle deposits and withdrawals. We will deliberately keep the code short, but it captures the important elements.

pragma solidity ^0.8.10;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SimplePool is ReentrancyGuard {
    mapping(address => uint256) public balances;
    uint256 public totalPool;

    function deposit() external payable {
        // 1. Checks
        require(msg.value > 0, "Nothing to deposit");

        // 2. Effects
        balances[msg.sender] += msg.value;
        totalPool += msg.value;

        // 3. Interactions – none. We keep this out of the function
    }

    function withdraw(uint256 amount) external nonReentrant {
        // 1. Checks
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // 2. Effects
        balances[msg.sender] -= amount;
        totalPool -= amount;

        // 3. Interactions
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

A couple of things to note here:

  • NonReentrant protects the entire withdraw operation. If an attacker tries to call withdraw during the call back into withdraw, the guard will revert.
  • We never use transfer because that function has a fixed gas stipend that could break for certain ERC20 tokens. The low‑level call is the recommended pattern, combined with a check on success.
  • The checks and effects happen first, before we allow any external calls. Even if someone sends a contract that calls back into withdraw, the balance has already been deducted.

The library that ships with this contract has already implemented the ReentrancyGuard. If you were to replace SimplePool with a third‑party lending library, you’d want to verify that the library includes the same pattern. If the library provides a withDraw that first interacts with the recipient before updating balances, that would be a red flag.

Reentrancy in the context of DeFi protocols

Big DeFi protocols spend a lot of effort on auditing and formal verification. A recent audit of the Aave protocol for the new v3 update found multiple functions that were refactored to use the pull over push pattern, eliminating the need for repeated calls.

Yet, the pattern of reentrancy is not limited to withdrawal functions. Attackers have also hacked Flash Loan adapters, leveraging the fact that many protocols allow the borrower to perform arbitrary logic before the loan is repaid. If a borrower’s logic causes a reentrancy attack before the repayment, the protocol could lose funds.

How to audit effectively

If you’re a developer or an auditor looking into a library, follow these simple steps:

  1. Pull the source. Do not rely on the bytecode alone. Look for the original Solidity code in GitHub repositories or audited releases. Open source transparency is a primary defense.
  2. Search for nonReentrant or ReentrancyGuard. This is a quick indicator. If the contract doesn’t use a guard at all, scan each public/external function.
  3. Read through each public function. Identify where external calls happen. Make a mental note of the order: checks → changes → interactions.
  4. Check for the pull pattern. If the contract pushes funds directly, look for state‑update after the call. If the update happens after, reentrancy is possible.
  5. Test with a simple reentrancy contract. Write a small attacker contract that calls the target function and re-enters during the external call. If the test succeeds (i.e., the attacker ends up with extra balance), you have found a vulnerability.

A personal side note

I once had a client who was building a stablecoin distribution platform. Their library used a well‑known yield‑optimisation protocol that performed a call to an external address before updating user balances. I suggested adding a ReentrancyGuard and reordering the function. The client was initially skeptical—“We want a fast execution path.” But after a quick audit and a mock reentrancy test, they realized that the potential loss would dwarf the minor gas cost increase. They went ahead with the guard, and a month later, when a new reentrancy exploit popped up in a competing protocol, they were able to move their liquidity elsewhere without losing a single token.

That reminds us of a simple truth: investing in security is like pruning a garden. A few extra minutes of trimming today keeps weeds from choking your plants tomorrow.

Bottom line

Reentrancy is not a sophisticated cryptographic loophole. It is a logical bug that arises when functions allow nested calls before their own state changes settle. The DeFi community has learned that the simplest way to guard against it is to check first, modify next, and only then interact. Libraries that adhere to this pattern—and that make the ReentrancyGuard a default—are a far safer foundation for building your own projects.

If you’re planning to integrate a DeFi library, pause, read the source, and ask these three questions:

  1. Does every external‑call‑making function update state before the call?
  2. Is there a lock or guard mechanism preventing re‑entrancy?
  3. Have recent audits highlighted reentrancy concerns?

Answer “yes” to each, and you’ll have at least one of the strongest shields in the blockchain world.

Let’s zoom out. Think of a contract as a garden. Each function is a plant that requires careful watering. If you water a plant and it starts a spiral of water flow back to itself, you’ll drown the garden. Checking, then updating, then interacting is like ensuring each plant gets the right amount of water at the right moment—no runoff into neighboring beds.

Your next move? Review the libraries you’re poised to use, make sure they follow these patterns, and then enjoy the peace of mind that comes with a solid, reentrancy‑free foundation.

Sofia Renz
Written by

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.

Contents