Reentrancy attacks happen when a program lets another program call back into it before finishing its work. This can cause unexpected problems like losing money or data.
Reentrancy attacks in Blockchain / Solidity
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount;
}This example shows a vulnerable withdraw function in Solidity.
The call to send money happens before updating the balance, which can be exploited.
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}bool locked; modifier noReentrancy() { require(!locked, "No reentrancy"); locked = true; _; locked = false; } function withdraw(uint amount) public noReentrancy { require(balances[msg.sender] >= amount); balances[msg.sender] -= amount; (bool success, ) = msg.sender.call{value: amount}(""); require(success); }
This contract lets users deposit and withdraw money. The withdraw function is vulnerable because it sends money before updating the balance.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; 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, "Insufficient balance"); (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); balances[msg.sender] -= amount; } }
Always update balances before sending money to prevent reentrancy.
Use a reentrancy guard (lock) to block repeated calls during execution.
Test your contracts with attack simulations to find vulnerabilities.
Reentrancy attacks happen when a contract calls back into itself before finishing.
Update state before sending money to avoid this attack.
Use locks or modifiers to protect your functions.
Practice
Solution
Step 1: Understand the concept of reentrancy
Reentrancy happens when a contract allows another call to itself before the first call finishes, causing unexpected behavior.Step 2: Identify the correct description
An attack where a contract is called repeatedly before finishing its first call correctly describes repeated calls before finishing, which is the core of reentrancy attacks.Final Answer:
An attack where a contract is called repeatedly before finishing its first call -> Option AQuick Check:
Repeated calls before finish = Reentrancy attack [OK]
- Confusing reentrancy with key theft
- Thinking it floods network instead
- Mixing with consensus attacks
Solution
Step 1: Identify the safe order of operations
To prevent reentrancy, update the user's balance before sending Ether to avoid repeated withdrawals.Step 2: Check each option's order
balances[msg.sender] = 0; payable(msg.sender).transfer(amount); updates balance first, then sends Ether, which is the correct pattern.Final Answer:
balances[msg.sender] = 0; payable(msg.sender).transfer(amount); -> Option CQuick Check:
Update state before transfer = Safe pattern [OK]
- Sending Ether before updating balance
- Not checking balance before transfer
- Increasing balance instead of resetting
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
payable(msg.sender).transfer(amount);
balances[msg.sender] -= amount;
}
What is the likely outcome if this contract is attacked by a reentrancy exploit?Solution
Step 1: Analyze the order of operations in withdraw
The contract sends Ether before reducing the balance, allowing reentrant calls to withdraw repeatedly.Step 2: Understand the attack effect
Because balance is reduced after transfer, attacker can call withdraw again before balance updates, draining funds.Final Answer:
The attacker can withdraw more than their balance multiple times -> Option AQuick Check:
Transfer before balance update = Vulnerable to reentrancy [OK]
- Assuming require protects fully
- Thinking syntax error occurs
- Believing attacker can't withdraw extra
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
payable(msg.sender).transfer(amount);
balances[msg.sender] -= amount;
}Solution
Step 1: Identify vulnerability cause
Balance is updated after sending Ether, allowing reentrant calls to exploit the contract.Step 2: Apply fix by reordering
Moving balance update before transfer prevents reentrancy by blocking repeated withdrawals.Final Answer:
Move balances[msg.sender] -= amount; before transfer call -> Option DQuick Check:
Update state before transfer = Fix reentrancy [OK]
- Removing require weakens security
- Adding loops increases risk
- Increasing balance after transfer worsens issue
Solution
Step 1: Recall best practices for reentrancy protection
Update contract state before external calls and use reentrancy guard modifiers to block repeated calls.Step 2: Evaluate options
Update state before external calls and use a reentrancy guard modifier combines both safe state update order and reentrancy guard, providing strong protection.Final Answer:
Update state before external calls and use a reentrancy guard modifier -> Option BQuick Check:
State update + guard = Best reentrancy defense [OK]
- Sending Ether before state update
- Ignoring reentrancy guard usage
- Allowing reentrant calls for speed
Ready to prove it?
Pass the test and this pattern moves unknown
