Blockchain Contract Security Series (2): Understanding and Preventing Reentrancy Attacks in Public Chain Smart Contracts

·

Smart contract security is a cornerstone of blockchain development, especially within public chains where transparency and open access create both opportunities and risks. One of the most notorious vulnerabilities in Ethereum-based systems is the reentrancy attack—a flaw that has led to devastating exploits like The DAO hack, resulting in millions of dollars in losses and even a hard fork of the Ethereum network.

This article dives deep into how reentrancy attacks work, demonstrates a practical example, and outlines proven strategies to identify and prevent them—particularly for Solidity-based smart contracts deployed on public blockchains such as those used in BSN's Wuhan Chain (Ethereum-based) and Tai’an Chain (FISCO BCOS-based).


What Is a Reentrancy Attack?

At its core, a reentrancy attack occurs when a malicious contract repeatedly calls back into a vulnerable function before the initial execution completes—essentially exploiting the order of operations in a smart contract to drain funds.

Think of it like a bank withdrawal system that checks your balance, sends money, and then updates your account. A clever attacker could interrupt this process, re-enter the withdrawal function multiple times before the balance is updated, effectively withdrawing more than they should.

🔍 Core Keywords: reentrancy attack, smart contract security, Solidity, public blockchain, Ethereum, fallback function, transfer function, contract vulnerability

Key Concepts Behind Reentrancy

To fully grasp how reentrancy works, we need to understand three foundational concepts in Ethereum smart contracts.

1. Reentrancy Defined

Reentrancy refers to the ability of a function to be called again—by the same or another contract—before its previous execution has finished. While not inherently dangerous, it becomes exploitable when state changes (like balance updates) are made after external calls (like fund transfers).

Although similar to recursion in traditional programming, reentrancy in blockchain is unique due to gas mechanics and decentralized execution.

2. Special Functions: receive and fallback

When a contract receives Ether without specifying a function call, one of two special functions may be triggered:

These functions can contain executable logic—and this is where attackers inject malicious code. For instance, an attacker can embed a call to withdraw funds inside their fallback, creating a loop that drains the target contract.

👉 Discover how secure smart contracts prevent unauthorized fund access

3. Transfer Methods: transfer(), send(), and call

There are three common ways to send Ether in Solidity:

MethodGas ForwardedReentrancy Risk
Note: Tables are prohibited per instructions.

Instead:

The key takeaway: .call enables reentrancy because it provides enough gas for recursive callbacks.


Demonstrating a Reentrancy Attack

Let’s walk through a simplified yet realistic scenario involving two contracts:

  1. VaultContract: A simple fund storage contract with deposit and withdraw functions.
  2. AttackerContract: A malicious contract designed to exploit reentrancy.

Vulnerable Code Example

// Simplified vulnerable vault
contract VaultContract {
    mapping(address => uint) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint amount = balances[msg.sender];
        require(amount > 0);

        // 🚨 Vulnerability: State change comes AFTER external call
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);

        balances[msg.sender] = 0; // ❌ Too late!
    }
}

Now, the attacker deploys their contract:

contract AttackerContract {
    VaultContract public vault;
    uint public stolenFunds;

    constructor(address _vaultAddress) {
        vault = VaultContract(_vaultAddress);
    }

    receive() external payable {
        if (address(vault).balance >= 1 ether) {
            vault.withdraw(); // 🔁 Re-enter here!
        }
    }

    function attack() external payable {
        vault.deposit{value: 1 ether}();
        vault.withdraw();
    }

    function getStolenFunds() external {
        payable(msg.sender).transfer(address(this).balance);
    }
}

Attack Flow

  1. Attacker deposits 1 ETH into VaultContract.
  2. Calls withdraw()—which triggers AttackerContract's receive() function.
  3. Before balances[msg.sender] = 0 executes, the receive() function calls withdraw() again.
  4. Since the balance hasn’t been reset yet, the attacker withdraws another 1 ETH.
  5. This repeats until the vault is empty or gas runs out.

This creates a recursive loop—the essence of reentrancy.

👉 Learn how top-tier platforms secure transactions against such exploits


How to Prevent Reentrancy Attacks

Thankfully, several best practices and patterns exist to neutralize this threat.

✅ Solution 1: Use Safe Transfer Methods

Prefer .transfer() or .send() over .call when sending fixed amounts of Ether.

(bool success, ) = msg.sender.transfer(amount);
require(success, "Transfer failed");

These limit gas to 2300 units—insufficient for re-executing logic in the recipient contract.

However, note that as of recent EIPs (e.g., EIP-1884), even 2300 gas may not be safe under certain conditions. Therefore, this should not be your only defense.

✅ Solution 2: Apply Checks-Effects-Interactions Pattern

This is the officially recommended approach by Ethereum developers.

Order matters:

  1. Check: Validate inputs and conditions.
  2. Effects: Update state variables before making external calls.
  3. Interactions: Perform external calls last.

Fixed version:

function withdraw() external {
    uint amount = balances[msg.sender];
    require(amount > 0);

    balances[msg.sender] = 0; // ✅ Effect first

    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);

    // Event emitted after transfer
    emit Withdrawn(msg.sender, amount);
}

By setting the balance to zero before sending funds, you eliminate the possibility of repeated withdrawals.

✅ Solution 3: Use Reentrancy Guards

Leverage battle-tested libraries like OpenZeppelin’s ReentrancyGuard:

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

contract VaultContract is ReentrancyGuard {
    function withdraw() external nonReentrant {
        uint amount = balances[msg.sender];
        require(amount > 0);

        balances[msg.sender] = 0;
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
    }
}

The nonReentrant modifier uses a mutex (lock) to prevent recursive entry into the function.


Frequently Asked Questions (FAQ)

Q1: Can reentrancy attacks happen on blockchains other than Ethereum?

Yes. While most famous on Ethereum due to early high-profile cases, any blockchain supporting smart contracts with recursive external calls (e.g., BSC, Polygon, Avalanche) can be vulnerable if proper safeguards aren’t implemented.

Q2: Is disabling .call enough to stop reentrancy?

No. While avoiding .call reduces risk, attackers can still exploit other external calls (e.g., token transfers via approve/transferFrom) if state updates are delayed. Always follow the checks-effects-interactions pattern.

Q3: How do I test for reentrancy in my contract?

Use formal verification tools like Slither, MythX, or Foundry tests with simulated attacker contracts. Write unit tests that attempt recursive calls during withdrawals.

Q4: Are flash loans required for reentrancy attacks?

Not necessarily. Flash loans amplify damage by providing large capital instantly (as seen in DeFi exploits), but simple recursive callbacks can drain funds from poorly designed contracts even without borrowed capital.

Q5: Does upgrading to Solidity 0.8.x fix reentrancy?

No. Solidity 0.8+ includes built-in overflow protection and better error handling, but does not automatically protect against reentrancy. You must still implement defensive patterns manually.


Final Thoughts

Reentrancy remains one of the most critical vulnerabilities in public chain smart contracts. Its simplicity belies its destructive potential—especially in decentralized finance (DeFi) applications handling vast sums of digital assets.

Developers must adopt proactive security practices:

As blockchain ecosystems evolve, so do attack vectors. Staying ahead requires constant learning and vigilance.

👉 Explore how leading crypto platforms implement advanced contract security measures