A reentrant function can be "re-entered" safely in the middle of execution, often (but not always) in a concurrent environment.
Reentrancy is a bit different in smart contract execution. For one, all state is global state. On the other hand, for most EVM implementations, there is no concurrency. However, reentrancy is fairly common, as contracts can arbitrarily call and execute code in other contracts.
The attack goes something like this:
- Attack contract A calls function
withdrawof contract B
- B makes partial state changes, then calls back to A (e.g., to
- Attack A uses a
fallbackmethod to overload the function call and re-enters
withdrawin contract B, before the original execution has finished.
The 2016 Ethereum hack of the DAO, which caused a network hard-fork and rollback, was due to a reentrancy attack. Here's a list of dozens of reentrancy attacks on GitHub.
A simple example:
The stack trace would look something like this
Attack.attack -> EtherStore.deposit 1 -> EtherStore.withdraw 1 -> -> Attack.transfer (fallback, attack) -> -> -> EtherStore.withdraw 1 -> -> -> -> Attack.transfer (fallback, attack) -> -> -> -> ...
A few distinctions:
- Reentrant functions are recursive, but not all recursive functions are reentrant
- Thread-safety vs. reentrancy – you can use language primitives to scope global variables to thread-local variables and have thread-safety, but not reentrancy in the same thread.
- Idempotence vs. reentrancy – idempotence means that the same function can be called multiple times with the same input and yield the same output.