1. Reentrancy
also known as or related to race to empty, recursive call vulnerability, call to the unknown
This exploit was missed in review so many times by so many different people: reviewers tend to review functions one at a time, and assume that calls to secure subroutines will operate securely and as intended.
The Reentrancy attack, probably the most famous Ethereum vulnerability, surprised everyone when discovered for the first time. It was first unveiled during a multimillion dollar heist which led to a hard fork of Ethereum. Reentrancy occurs when external contract calls are allowed to make new calls to the calling contract before the initial execution is complete. For a function, this means that the contract state may change in the middle of its execution as a result of a call to an untrusted contract or the use of a low level function with an external address.
Loss: estimated at 3.5M ETH (~50M USD at the time)
Timeline of discovery:
Real World Impact:
Example:
- A smart contract tracks the balance of a number of external addresses and allows users to retrieve funds with its public
withdraw()
function. - A malicious smart contract uses the
withdraw()
function to retrieve its entire balance. - The victim contract executes the
call.value(amount)()
low level function to send the ether to the malicious contract before updating the balance of the malicious contract. - The malicious contract has a payable
fallback()
function that accepts the funds and then calls back into the victim contract'swithdraw()
function. - This second execution triggers a transfer of funds: remember, the balance of the malicious contract still hasn't been updated from the first withdrawal. As a result, the malicious contract successfully withdraws its entire balance a second time.
Code Example:
The following function contains a function vulnerable to a reentrancy attack. When the low level call()
function sends ether to the msg.sender
address, it becomes vulnerable; if the address is a smart contract, the payment will trigger its fallback function with what's left of the transaction gas:
function withdraw(uint _amount) {
require(balances[msg.sender] >= _amount);
msg.sender.call.value(_amount)();
balances[msg.sender] -= _amount;
}
Additional Resources: