Introduction
- In this lesson, you'll learn design patterns that help mitigate Reentrancy attacks. After you've learned the theory, you'll fix a Reentrancy bug in the lab environment!
- Do you already have a grasp on how to defend against Reentrancy attacks? Feel free to move to the Reentrancy Fundamentals: Defend Lab and put your knowledge to the test!Tip
Prerequisites
Initialize Experiment Lab Environment
- Navigate to the Lab Options section.
- Select the environment that's right for you: Github Codespace (recommended) or Visual Studio Code.
- In the Next Steps section, only complete Step 1 (Initialize Lab).Important:
Please do NOT complete any other steps.
Code
23 collapsed lines
// SPDX-License-Identifier: MIT// WARNING: This is buggy code. Do NOT use in the real world.
pragma solidity 0.8.24;
import "./shared/VulnerableBase.sol";
contract Vulnerable is VulnerableBase { mapping(address => uint256) public balances;
constructor() payable VulnerableBase() {}
// Allow a customer the ability to deposit funds into their account function deposit() public payable { uint256 initialBalance = balances[msg.sender]; balances[msg.sender] += msg.value; emit AttackerDepositBalance( initialBalance / 1 ether, balances[msg.sender] / 1 ether ); emit VulnerableContractBalance(address(this).balance / 1 ether); }
// Allow a customer the ability to withdraw all ETH from their account. function withdrawAll() public { uint256 beginExecutionBalance = balances[msg.sender]; // COMMENT GROUP A: START if (beginExecutionBalance <= 0) { revert("Customer has insufficient balance"); } (bool success, ) = msg.sender.call{value: beginExecutionBalance}(""); require(success, "Failed to send ETH"); balances[msg.sender] = 0; // COMMENT GROUP A: END }}
General considerations
These considerations apply regardless of what Defense Pattern you choose.
Invariants
- Invariants are properties of the program state that should always be true.
- Think of invariants like accounting rules. For example, your bank account's records should always match the actual money in the account. If there's ever a mismatch, something is wrong.
- As you write code, it's vital to document what your invariants are. Within the
Vulnerable
contract, the primary invariant is: the sum of allbalances
should equal the contract'sETH
balance. - Invariants can break for brief moments of time. For example, the time between MarkerMental Model
1
and Marker2
. When you spot this violation, it's a hint that you should think about various edge cases. In particular, you should ponder: "How would a financially motivated Attacker exploit this condition?" In this case,Vulnerable
Contract calls into an untrusted contract (Attacker
Contract) while invariants are violated. This gives theAttacker
Contract the ability to stealETH
.
Defense Pattern 1: Gas Restrictions
-
One way to learn about an "ideal" implementation is to learn non-ideal variants. As you experiment with this pattern, reflect on how this pattern might NOT be ideal.Reflection
Code
- Green highlights: Correspond to additions.
- Red highlights: Correspond to removals.
23 collapsed lines
// SPDX-License-Identifier: MIT // WARNING: This is buggy code. Do NOT use in the real world.
pragma solidity 0.8.24;
import "./shared/VulnerableBase.sol";
contract Vulnerable is VulnerableBase { mapping(address => uint256) public balances;
constructor() payable VulnerableBase() {}
// Allow a customer the ability to deposit funds into their account function deposit() public payable { uint256 initialBalance = balances[msg.sender]; balances[msg.sender] += msg.value; emit AttackerDepositBalance( initialBalance / 1 ether, balances[msg.sender] / 1 ether ); emit VulnerableContractBalance(address(this).balance / 1 ether); }
// Allow a customer the ability to withdraw all ETH from their account. function withdrawAll() public { uint256 beginExecutionBalance = balances[msg.sender]; // COMMENT GROUP A: START if (beginExecutionBalance <= 0) { revert("Customer has insufficient balance"); } (bool success, ) = msg.sender.call{value: beginExecutionBalance}(""); bool success = payable(msg.sender).send(beginExecutionBalance); require(success, "Failed to send ETH"); balances[msg.sender] = 0; // COMMENT GROUP A: END } }
Challenge
- If you haven't done so already, complete the steps within the Initialize Experiment Lab Environment section.
- This will open the lab environment.
- OPTIONAL: If you previously opened the lab environment and made changes to
Vulnerable.sol
, you'll need to closeVulnerable.sol
and run therevert
command within the lab environment's terminal. - In the lab environment, open
Vulnerable.sol
. - Apply the code diff (above) to
Vulnerable.sol
.- Before moving forward, try to predict how this will affect the Reentrancy vulnerability.Reflection
- In the lab environment's terminal, execute
cv
.- Behind the scenes, the
cv
command will automatically invokeAttacker.attack()
.
- Behind the scenes, the
- Try your best to answer the questions (below).
Questions
While it may seem counterintuitive, often the best time to receive a quiz is before you "officially" learn the content. Even if you don't answer "correctly", this process can strengthen your knowledge retention. Try your best to answer the questions below before viewing the Answer section.
- The code changes prevented the Reentrancy attack. Why did this occur?
- As it relates to Reentrancy, can you think of any edge cases with the
send()
coding pattern?
Defense Pattern 2: Reentrancy Guard
A Reentrancy guard is a piece of code that causes execution to fail when Reentrancy is detected. There is an implementation of this pattern in OpenZeppelin Contracts called ReentrancyGuard, which provides the
nonReentrant
modifier. Applying this modifier to a function will render it “non-reentrant”, and attempts to re-enter this function will be rejected by reverting the call.
Code
In the code (below), you'll see an updated
Vulnerable.sol
that exhibits the Reentrancy guard pattern.
- Green highlights: Correspond to additions.
- Red highlights: Correspond to removals.
// SPDX-License-Identifier: MIT // WARNING: This is buggy code. Do NOT use in the real world.
pragma solidity 0.8.24;
import "./shared/VulnerableBase.sol";import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract Vulnerable is VulnerableBase {contract Vulnerable is VulnerableBase, ReentrancyGuard { mapping(address => uint256) public balances;
constructor() payable VulnerableBase() {}
// Allow a customer the ability to deposit funds into their account function deposit() public payable { function deposit() public payable nonReentrant { uint256 initialBalance = balances[msg.sender]; balances[msg.sender] += msg.value; emit AttackerDepositBalance( initialBalance / 1 ether, balances[msg.sender] / 1 ether ); emit VulnerableContractBalance(address(this).balance / 1 ether); }
// Allow a customer the ability to withdraw all ETH from their account. function withdrawAll() public { function withdrawAll() public nonReentrant { uint256 beginExecutionBalance = balances[msg.sender]; // COMMENT GROUP A: START if (beginExecutionBalance <= 0) { revert("Customer has insufficient balance"); } (bool success, ) = msg.sender.call{value: beginExecutionBalance}(""); require(success, "Failed to send ETH"); balances[msg.sender] = 0; // COMMENT GROUP A: END } }
nonReentrant
works like a simple lock system. When a function with the nonReentrant
modifier is called, ReentrancyGuard.sol
will (behind the scenes) update a _status
state variable. You can think of _status
as holding the lock/unlocked status.
When a
nonReentrant
function starts executing, it sets a lock (changing
_status
from NOT_ENTERED
to ENTERED
). If another function (in the call stack) tries to call ANY of the contract's nonReentrant
functions, that call will be reverted. Once the function finishes execution, it removes the lock (setting _status
back to NOT_ENTERED
).
Read the
ReentrancyGuard.sol source to learn more.In the upcoming questions/answers, you'll dive deeper into the nuances of
RentrancyGuard
including why nonReentrant
is leveraged on Vulnerable.deposit
.
Challenge
- If you haven't done so already, complete the steps within the Initialize Experiment Lab Environment section.
- This will open the lab environment.
- OPTIONAL: If you previously opened the lab environment and made changes to
Vulnerable.sol
, you'll need to closeVulnerable.sol
and run therevert
command within the lab environment's terminal. - In the lab environment, open
Vulnerable.sol
. - Apply the code diff (above) to
Vulnerable.sol
.- Before moving forward, try to predict how this will affect the Reentrancy vulnerabilityReflection
- In the lab environment's terminal, execute
cv
.- Behind the scenes, the
cv
command will callAttacker.attack()
.
- Behind the scenes, the
- Try your best to answer the questions (below).
Questions
- After executing
cv
, you'll notice thatAttacker.sol
wasn't able to steal anyETH
fromVulnerable.sol
. AlthoughnonReentrant
worked in this example, can this pattern be cumbersome to maintain?
- Could this code have any other Reentrancy vulnerabilities?
Summary
- In the ideal world, you would not violate invariants while calling untrusted contracts. However, if you can't avoid this situation,
ReentrancyGuard
is a pattern that you can explore. As mentioned,ReentrancyGuard
might produce unexpected outcomes and if you implement it, you'll need to be mindful of its challenges. For simplicity, allReentrancyGuard
challenges were not outlined in this lesson.
Defense Pattern 3: Checks-Effect-Interactions
The best way to combat Reentrancy vulnerabilities is to leverage the Checks-Effects-Interactions (CEI) pattern.
Using Checks-Effects-Interactions (CEI):
- Classify all code statements as a Check, Effect or Interaction.
- Checks will check a condition.
- Effects will affect the contract state.
- Interactions will interact with an external contract.
- While coding, ensure the following order is preserved: Checks (occurs before) Effects (occurs before) Interactions.
The primary benefit of CEI (compared to other defensive patterns) is that it fixes the "core" problem. When CEI is properly implemented, attackers cannot exploit inconsistent contract states.
What are the pros and cons of this pattern?
- Pros:
- Simple to reason about.
- Applies to all Reentrancy variants (e.g., Single-Function Reentrancy, Cross-Function Reentrancy, Cross-Contract Reentrancy, etc.)
- All of these variants have been covered within this page's Answer sections or within Reentrancy Fundamentals: Attack Theory.Tip
- Cons: Prone to error. All code statements need to be classified and ordered correctly. What occurs if someone forgot to add the correct ordering? Or what happens if a linter changes the order of code statements? To counteract these issues, you can use
slither
to help detect Reentrancy bugs as well as other security issues. You'll learn more aboutslither
in the upcoming paragraphs.
In the next experimental challenge, you'll leverage
slither
to detect a CEI violation. slither
is a Solidity & Vyper static analysis framework maintained by Trail Of Bits. It runs a suite of vulnerability detectors, prints visual information about contract details, and provides an API to easily write custom analyses. slither
enables developers to find vulnerabilities, enhance their code comprehension, and quickly prototype custom analyses.2
- If you haven't done so already, complete the steps within the Initialize Experiment Lab Environment section.
- This will open the lab environment.
- OPTIONAL: If you previously made changes to
Vulnerable.sol
, you'll need to closeVulnerable.sol
and run therevert
command within the lab environment's terminal. - Try your best to answer the questions (below).
Questions
- Navigate to the Code section and focus on
withdrawAll()
. Where applicable, classify all code statements as a Check, Effect or Interaction. What order did you observe?
- In the lab's terminal, execute
slither $BLOCKBASH_WORKSPACE_DIR_PATH/Vulnerable.sol
. Is the output expected?
Key Takeaways
- Reentrancy can occur in various forms (e.g., Single-Function Reentrancy, Cross-Function Reentrancy, Cross-Contract Reentrancy, Read-Only Reentrancy).
- The best way to combat Reentrancy vulnerabilities is to leverage the Checks-Effects-Interactions (CEI) pattern.
- Checks-Effects-Interactions can be prone to error. While no tool is perfect, you can leverage
slither
in your CI pipeline to notify you of potential CEI violations.
Next Steps
- In the
Reentrancy Fundamentals: Defend Lab, you'll update
Vulnerable.sol
for the Checks-Effects-Interactions pattern.
Real World Examples
Inspiration
We wanted to thank the content creators who published the content below. Without people like you, BlockBash tutorials couldn't exist :)
- Reentrancy After Istanbul
- Read-only Reentrancy: In-Depth
- Read-only reentrancy attacks: understanding the threat to your smart contract
- Smart Contract Security Field Guide: Reentrancy
- The Ultimate Guide To Reentrancy
- Fuzz / Invariant Tests | The New Bare Minimum For Smart Contract Security
- The Full Guide on Reentrancy Attacks in Solidity Smart Contracts