Reentrancy describes any situation where a smart contract performs an external call (to another contract or address), and the callee can call back into the original contract before the first invocation has completed and state has been fully updated. If the caller is not designed to be reentrancy-safe, the callback can observe stale state and exploit it—e.g., withdrawing more than the caller’s balance, double-counting rewards, or manipulating accounting across complex multi-step flows.
This affects all contract types that perform external calls: DeFi (token transfers, DEX swaps, vault deposits/withdrawals, flash loan callbacks), NFTs (transfers with ERC-721/ERC-1155 receiver hooks, marketplace payouts), DAOs (proposal execution that invokes external contracts), bridges (message relay, asset transfers), and composable protocols (ERC-777 hooks, ERC-4626 deposit/withdraw hooks). Reentrancy can be single-function (same function called recursively), cross-function (callback into a different function), or cross-contract (callback traverses multiple contracts). On non-EVM chains, analogous patterns exist wherever cross-program invocations can recurse.
Few areas to focus on:
Attackers exploit:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IToken {
function transfer(address to, uint256 amount) external returns (bool);
}
contract VulnerableVault {
IToken public immutable token;
mapping(address => uint256) public balances;
constructor(IToken _token) {
token = _token;
}
function deposit(uint256 amount) external {
// Assume token already transferred in for brevity
balances[msg.sender] += amount;
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "insufficient");
// External call before state update – reentrancy window
token.transfer(msg.sender, amount);
balances[msg.sender] -= amount;
}
}
Issues:
withdraw from within the transfer call, repeatedly withdrawing based on the unchanged balances[msg.sender].// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
interface ITokenSafe {
function transfer(address to, uint256 amount) external returns (bool);
}
contract SafeVault is ReentrancyGuard {
ITokenSafe public immutable token;
mapping(address => uint256) public balances;
error InsufficientBalance();
error TransferFailed();
constructor(ITokenSafe _token) {
token = _token;
}
function deposit(uint256 amount) external {
// Assume token already transferred in for brevity
balances[msg.sender] += amount;
}
function withdraw(uint256 amount) external nonReentrant {
uint256 bal = balances[msg.sender];
if (bal < amount) revert InsufficientBalance();
// Effects first
balances[msg.sender] = bal - amount;
// Then interaction
bool ok = token.transfer(msg.sender, amount);
if (!ok) revert TransferFailed();
}
}
Security Improvements:
nonReentrant modifier from OpenZeppelin’s ReentrancyGuard.executeDecreaseOrder. The function accepted the attacker’s smart contract address as a parameter; when it transferred control to that address during the refund process, the attacker re-entered and manipulated global average short prices, AUM, and GLP valuations. State updates after external calls and lack of reentrancy guards enabled the drain. The vulnerability was introduced in 2022 as an unaudited patch.
ReentrancyGuard or similar reentrancy locks on stateful functions that:
deposit calling withdraw internally).