Consider the following simplified Solidity contract vulnerable to reentrancy:
contract Vulnerable {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
(bool success, ) = msg.sender.call{value: _amount}("");
require(success);
balances[msg.sender] -= _amount;
}
}If an attacker calls withdraw with a crafted fallback function that calls withdraw again before the balance is updated, what will be the final balance of the attacker after one initial withdrawal of 1 ether?
Think about when the balance is updated relative to the external call.
The contract sends ether before updating the balance, so the attacker can recursively call withdraw multiple times, draining more ether than they deposited.
Which of the following is the best practice to prevent reentrancy attacks in Solidity smart contracts?
Think about the order of state changes and external calls.
Updating the balance before sending ether ensures the contract state is correct before any external call, preventing reentrancy exploits.
Examine the following Solidity function and identify the vulnerability:
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
(bool success, ) = msg.sender.call{value: _amount}("");
require(success);
balances[msg.sender] -= _amount;
}Consider the order of operations and what happens if the external call triggers fallback code.
Updating balances after sending ether allows attackers to reenter the function and withdraw multiple times before the balance is decreased.
Given the vulnerable withdraw function below, which option correctly fixes the reentrancy vulnerability by changing the order of operations?
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
(bool success, ) = msg.sender.call{value: _amount}("");
require(success);
balances[msg.sender] -= _amount;
}Focus on the order of balance update and external call.
Updating the balance before the external call prevents attackers from reentering with an unchanged balance.
An attacker deposits 5 ether into the vulnerable contract below and then calls withdraw(1 ether). The fallback function recursively calls withdraw(1 ether) as long as the balance is sufficient.
contract Vulnerable {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
(bool success, ) = msg.sender.call{value: _amount}("");
require(success);
balances[msg.sender] -= _amount;
}
}How many times will the attacker be able to withdraw 1 ether recursively before the transaction fails or completes?
Think about when the balance is decreased relative to the external call and recursive calls.
Because the balance is decreased after the external call, the attacker can recursively withdraw multiple times before the balance is updated, effectively withdrawing more than the deposited amount.