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:
Classic withdraw-before-update (state change after external call)
Cross-function reentrancy (read-your-writes assumptions between functions)
Read-only reentrancy (view functions or oracles reading state during a callback)
Cross-contract and multi-module reentrancy (vault → strategy → DEX flows)
Attackers exploit:
Malicious tokens or receivers that re-enter on transfer/hook callbacks
Flash loan callbacks that execute attacker logic before repayment
Complex call graphs where state is inconsistent mid-transaction across modules
事例 (脆弱な再入可能性パターン)
// SPDX-License-Identifier: MITpragmasolidity^0.8.20;interface IToken{functiontransfer(addressto,uint256amount)externalreturns(bool);}contract VulnerableVault{ IToken publicimmutable token;mapping(address=>uint256)public balances;constructor(IToken_token) { token = _token;}functiondeposit(uint256amount)external{// Assume token already transferred in for brevity balances[msg.sender]+= amount;}functionwithdraw(uint256amount)external{require(balances[msg.sender]>= amount,"insufficient");// External call before state update – reentrancy window token.transfer(msg.sender, amount); balances[msg.sender]-= amount;}}
Issues:
An attacker using a malicious token / proxy can re-enter withdraw from within the transfer call, repeatedly withdrawing based on the unchanged balances[msg.sender].
事例 (再入安全な引き落とし)
Security Improvements:
Uses nonReentrant modifier from OpenZeppelin’s ReentrancyGuard.
Applies checks-effects-interactions pattern to minimize reentrancy windows.
Checks transfer return value and reverts on failure.
2025 ケーススタディ
GMX (July 2025, $42M loss)
GMX V1 contracts were exploited via a classic yet sophisticated reentrancy vector in 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.