This guide provides a comprehensive analysis of reentrancy vulnerabilities with practical, executable examples for each attack type. Learn how different reentrancy patterns work and how to identify them in real protocols.

What is reentrancy?

Reentrancy occurs when an external contract call recursively calls back into the calling function before it completes execution. During external calls, control passes to the recipient contract, which can execute arbitrary code. If state updates happen after the external call, attackers can re-enter the function and drain funds.

Reentrancy hacks are still happening today. For a comprehensive list of historical reentrancy exploits, see pcaversaccio’s reentrancy-attacks repository.

Any contract that makes external calls is potentially vulnerable, especially those related to withdrawal mechanisms, DeFi protocols, and cross-chain bridges.

How to prevent reentrancies?

Follow the Checks-Effects-Interactions (CEI) pattern: perform checks, update state, and only then make external calls. This ordering prevents exploitation of inconsistent states.

ReentrancyGuard prevents single-contract reentrancy but not cross-contract attacks. Use pull payment patterns and minimise external calls for better security.

Wake‘s Python-based framework includes reentrancy detection through static analysis and testing patterns, identifying vulnerabilities before deployment.

Identifying reentrancy vulnerabilities

Key attack vectors include:

  • Functions making external calls before state updates
  • Token standards with hooks (ERC-777, ERC-1155)
  • Flash loan implementations
  • Cross-chain messaging systems

Every external call is a potential vulnerability point.

Types of reentrancy attacks

Reentrancy ranges from simple recursive calls to complex multi-contract exploits. Each type requires different defensive strategies. Developers must consider all attack possibilities during development. Let’s take a closer look at the mechanics of these critical vulnerabilities.

Single-function reentrancy

In a single-function reentrancy, attackers recursively call the same function during external calls. It is most common in withdrawal functions that send ETH before updating balances.

Example vulnerability:

function withdraw() public {
		uint256 amount = balances[msg.sender];
		(bool success, ) = msg.sender.call{value: amount}("");
		require(success, "Failed to send Ether");
		balances[msg.sender] = 0; // State updated after external call
}

Attack steps:

  1. Attacker deposits funds and calls withdraw()
  2. During the external call, attacker’s contract re-enters withdraw()
  3. Balance hasn’t been reset yet, so withdrawal succeeds again
  4. Process repeats until contract is drained

Prevention: Update state before external calls following the CEI pattern.

Cross-function reentrancy

Cross-function reentrancy exploits multiple functions within one contract. It gets dangerous when developers protect individual functions but miss their interactions.

Example vulnerability:

contract Vault is ReentrancyGuard {
		function transfer(address to, uint256 amount) public {
				// No reentrancy protection here
				if (balances[msg.sender] >= amount) {
						balances[to] += amount;
						balances[msg.sender] -= amount;
				}
		}
		function withdraw() public nonReentrant {
				uint256 amount = balances[msg.sender];
				(bool success, ) = msg.sender.call{value: amount}("");
				require(success, "Failed to send Ether");
				balances[msg.sender] = 0;
		}
}

Attack vector: Attacker re-enters through unprotected transfer() during withdraw() external call, manipulating balances before the withdrawal completes.

Prevention: Apply reentrancy guards to all state-modifying functions or use CEI pattern consistently.

Cross-chain reentrancy

As the name implies, cross-chain reentrancy manipulates cross-chain messaging to mint duplicate tokens across chains. This makes it critical for bridge implementations.

Vulnerability pattern: Cross-chain contracts often increment counters or update state after external calls like _safeMint(). If an attacker re-enters during the external call, they can trigger the same cross-chain event multiple times before state updates.

Real impact: Same tokenId minted on multiple chains, breaking bridge accounting and creating phantom tokens.

Prevention:

  • Update cross-chain state variables before external calls
  • Implement post-call verification of critical variables
  • Use reentrancy guards on cross-chain functions

Cross-contract reentrancy

Cross-contract reentrancy spans multiple contracts, bypassing ReentrancyGuard. This means it requires comprehensive interaction analysis.

Attack pattern: Contract A protected with ReentrancyGuard calls Contract B. During Contract A’s external call, attacker re-enters through Contract B’s unprotected functions, manipulating shared state.

Example scenario: Vault contract with ReentrancyGuard uses separate Token contract. Attacker re-enters through Token contract’s transfer function during Vault’s withdrawal process.

Prevention: Implement consistent reentrancy protection across all interacting contracts and follow CEI pattern in contract interactions.

Read-only reentrancy

Read-only reentrancy exploits view functions during state transitions. This makes it critical in DeFi protocols relying on price oracles.

Vulnerability mechanism: View functions appear safe but can return inconsistent data during state transitions. Attackers exploit this window to get incorrect price calculations.

Example scenario:

function getCurrentPrice() public view returns (uint256) {
		if (totalTokens == 0) return 10e18;
		return (totalTokens * 10e18) / totalStake;
}

During external calls, totalStake might be updated while totalTokens remains unchanged, creating price manipulation opportunities.

Prevention: Avoid view functions dependent on mutable state during external calls. Implement state consistency checks.

Protocol-specific vulnerabilities

Flash loan reentrancy

In a flash loan reentrancy, attackers manipulate token balances during loan execution by exploiting improper balance verification. Creates “side entrance” attacks where attackers use flash loans to temporarily inflate balances, bypass checks, then extract funds through different withdrawal mechanisms.

Common pattern: Flash loan increases user’s balance, triggering withdrawal logic that doesn’t account for the temporary balance inflation.

ERC-721 reentrancy

The onERC721Received callback enables attacks during NFT transfers. Critical for marketplaces and staking contracts where NFT transfers trigger state changes or payments.

Attack vector: Malicious contracts can re-enter during _safeMint or safeTransferFrom calls, manipulating contract state before NFT transfer completes.

ERC-777 reentrancy

In an ERC-777 reentrancy, transfer hooks break CEI patterns, enabling price manipulation. Advanced token features increase vulnerability surface area.

Mechanism: tokensReceived hooks execute during transfers, allowing attackers to re-enter sending contracts and manipulate state mid-transfer.

ERC-1155 reentrancy

Multi-token callbacks (onERC1155Received, onERC1155BatchReceived) create attack vectors in blockchain protocols.

Complexity factor: Batch operations create multiple reentrancy opportunities within single transactions, requiring careful state management.

Conclusion

Every external call is a potential reentrancy vulnerability. Follow CEI patterns, understand ReentrancyGuard limitations, and analyse all contract interactions.

As protocols become more complex with cross-chain bridges and advanced token standards, new attack patterns emerge. Security-first development is essential.

Modern DeFi protocols require comprehensive reentrancy analysis beyond a single-contract scope. Consider cross-contract interactions, token hooks, and read-only function dependencies when designing secure systems.

We hope that this overview will help you write safer code. Every reentrancy attack mentioned above has a deep-dive article linked in its respective section, so read on to learn more.