Reentrancy Attacks

Reentrancy Attacks


Reentrancy is one of the most critical and dangerous vulnerabilities in smart contracts. It occurs when a function makes an external call to another untrusted contract before resolving its own state.


What is Reentrancy?


A reentrancy attack happens when:

1. Contract A calls Contract B

2. Contract B calls back into Contract A before the first call completes

3. The state in Contract A hasn't been updated yet

4. Contract B exploits this to drain funds or manipulate state


Classic Example: The DAO Hack


The infamous DAO hack in 2016 exploited a reentrancy vulnerability, resulting in the loss of ~$60 million worth of ETH and leading to the Ethereum hard fork.


Vulnerable Pattern


function withdraw(uint amount) public {
    require(balances[msg.sender] >= amount);
    
    // Vulnerable: External call before state update
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
    
    balances[msg.sender] -= amount; // State update happens AFTER the call
}

Attack Scenario


contract Attacker {
    VulnerableContract target;
    
    function attack() external payable {
        target.deposit{value: 1 ether}();
        target.withdraw(1 ether);
    }
    
    // Fallback function that re-enters
    receive() external payable {
        if (address(target).balance >= 1 ether) {
            target.withdraw(1 ether);
        }
    }
}

Prevention Methods


1. Checks-Effects-Interactions Pattern


Always follow this order:

- **Checks**: Validate all conditions

- **Effects**: Update all state variables

- **Interactions**: Make external calls


function withdraw(uint amount) public {
    // Checks
    require(balances[msg.sender] >= amount);
    
    // Effects (update state FIRST)
    balances[msg.sender] -= amount;
    
    // Interactions (external call LAST)
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
}

2. ReentrancyGuard Modifier


Use OpenZeppelin's ReentrancyGuard:


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

contract MyContract is ReentrancyGuard {
    mapping(address => uint) public balances;
    
    function withdraw(uint amount) public nonReentrant {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
    }
}

3. Pull Payment Pattern


Instead of pushing payments, let users pull their funds:


mapping(address => uint) public pendingWithdrawals;

function initiateWithdrawal(uint amount) public {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;
    pendingWithdrawals[msg.sender] += amount;
}

function withdraw() public {
    uint amount = pendingWithdrawals[msg.sender];
    require(amount > 0);
    pendingWithdrawals[msg.sender] = 0;
    payable(msg.sender).transfer(amount);
}

Best Practices


1. Always update state before external calls

2. Use ReentrancyGuard for critical functions

3. Limit the gas forwarded to external calls

4. Use `.transfer()` or `.send()` instead of `.call{value: }()` when possible

5. Implement withdrawal patterns instead of push payments

6. Test thoroughly with tools like Slither and Echidna


Detection Tools


- **Slither**: Static analysis tool that detects reentrancy

- **Mythril**: Symbolic execution tool

- **Manticore**: Dynamic analysis

- **Foundry Fuzz**: Fuzz testing framework


References


- [Consensys Smart Contract Best Practices](https://consensys.github.io/smart-contract-best-practices/attacks/reentrancy/)

- [SWC-107: Reentrancy](https://swcregistry.io/docs/SWC-107)

Need Professional Security Audit?

Our experts can help secure your smart contracts

Get Audit Quote