How Reentrancy Actually Works, and Three Modern Defenses

Reentrancy is a critical security vulnerability in smart contracts where an external contract calls back into the current contract before the original function has completed. This

How Reentrancy Actually Works, and Three Modern Defenses

Reentrancy is a critical security vulnerability in smart contracts where an external contract calls back into the current contract before the original function has completed. This can lead to unexpected behavior, such as double-spending or state inconsistencies.

At its core, reentrancy occurs when a contract calls an external function (e.g., transfer to a user's wallet) and that external function then calls back into the original contract before the initial function has finished executing. This can allow an attacker to trigger malicious code in the middle of a state transition.

For example, consider a function that transfers ETH and updates a balance. If the external contract calls transfer and then re-enters the original contract, it may bypass the balance update, leading to a vulnerability.

1. Checks-Effects-Interactions Pattern

The checks-effects-interactions pattern is a proactive defense against reentrancy. It ensures that all state checks are performed first, followed by state changes, and finally any external calls.

function withdraw() public {
    require(balances[msg.sender] > 0, "No balance");
    balances[msg.sender] = 0; // State change
    (bool success, ) = msg.sender.call{value: balances[msg.sender]}("");
    require(success, "Transfer failed");
}

This pattern minimizes the window of opportunity for reentrancy by separating logic from external calls.

2. ReentrancyGuard

The ReentrancyGuard pattern uses a mutex (a boolean flag) to prevent reentrancy. It ensures that a function cannot be re-entered while it is already executing.

contract ReentrancyGuard {
    bool private _reentrancyGuard = true;

    modifier noReentrancy() {
        require(!_reentrancyGuard, "ReentrancyGuard: reentrant call");
        _reentrancyGuard = true;
        _;
        _reentrancyGuard = false;
    }
}

This pattern is effective but can have subtle edge cases, such as if the guard is not properly reset.

3. Pull Payments

Pull payments shift the responsibility of transferring funds from the contract to the user. Instead of sending ETH directly, the contract stores the amount and allows the user to withdraw it at their own discretion.

mapping(address => uint256) public balances;

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

function withdraw() public {
    uint256 amount = balances[msg.sender];
    balances[msg.sender] = 0;
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed");
}

This pattern eliminates the risk of reentrancy entirely by avoiding direct external calls.

A free contract scan is available from SparkAudit at sparkaudit.loca.lt.


Free smart-contract scan + AI audit (and a paid deep audit with exact patches) at sparkaudit.loca.lt - an AI auditor running on a private NVIDIA DGX Spark.

#smartcontracts #solidity #security #defi