Skip to main content

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!
  • Tip
    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!

Prerequisites

  • Reentrancy Fundamentals: Attack Theory

Initialize Experiment Lab Environment

  1. Navigate to the Lab Options section.
  2. Select the environment that's right for you: Github Codespace (recommended) or Visual Studio Code.
  3. In the Next Steps section, only complete Step 1 (Initialize Lab).
    Important:

    Please do NOT complete any other steps.

Code

Vulnerable.sol
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

Tip:

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 all balances should equal the contract's ETH balance.
  • Mental Model
    Invariants can break for brief moments of time. For example, the time between Marker 1 and Marker 2. 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 the Attacker Contract the ability to steal ETH.

Defense Pattern 1: Gas Restrictions

  • Reflection
    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.

Code

Tip

  • Green highlights: Correspond to additions.
  • Red highlights: Correspond to removals.

Vulnerable.sol
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

Experimental Challenge

  1. If you haven't done so already, complete the steps within the Initialize Experiment Lab Environment section.
    • This will open the lab environment.
  2. OPTIONAL: If you previously opened the lab environment and made changes to Vulnerable.sol, you'll need to close Vulnerable.sol and run the revert command within the lab environment's terminal.
  3. In the lab environment, open Vulnerable.sol.
  4. Apply the code diff (above) to Vulnerable.sol.
    • Reflection
      Before moving forward, try to predict how this will affect the Reentrancy vulnerability.
  5. In the lab environment's terminal, execute cv.
    • Behind the scenes, the cv command will automatically invoke Attacker.attack().
  6. Try your best to answer the questions (below).

Questions

Tip:

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.

  1. The code changes prevented the Reentrancy attack. Why did this occur?
  1. 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.

Tip

  • Green highlights: Correspond to additions.
  • Red highlights: Correspond to removals.

Vulnerable.sol
// 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

Experimental Challenge

  1. If you haven't done so already, complete the steps within the Initialize Experiment Lab Environment section.
    • This will open the lab environment.
  2. OPTIONAL: If you previously opened the lab environment and made changes to Vulnerable.sol, you'll need to close Vulnerable.sol and run the revert command within the lab environment's terminal.
  3. In the lab environment, open Vulnerable.sol.
  4. Apply the code diff (above) to Vulnerable.sol.
    • Reflection
      Before moving forward, try to predict how this will affect the Reentrancy vulnerability
  5. In the lab environment's terminal, execute cv.
    • Behind the scenes, the cv command will call Attacker.attack().
  6. Try your best to answer the questions (below).

Questions

  1. After executing cv, you'll notice that Attacker.sol wasn't able to steal any ETH from Vulnerable.sol. Although nonReentrant worked in this example, can this pattern be cumbersome to maintain?
  1. 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, all ReentrancyGuard 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):

  1. 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.
  2. 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.)
      • Tip
        All of these variants have been covered within this page's Answer sections or within Reentrancy Fundamentals: Attack Theory.
  • 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 about slither in the upcoming paragraphs.

Mental Model
Bugs happen. Always assume that a security protection will fail. At a minimum, you should have 2 security protections for every major vulnerability class. When you have multiple protections, you're applying defense-in-depth. Defense-in-depth is like protecting a medieval castle - no single defense is perfect, so multiple layers work together. The moat might freeze over, the guards might be bribed, but an Attacker would need to overcome ALL of these independent defenses to come inside. By having multiple varied defenses, even if one layer fails (or has "bugs"), the others continue protecting your valuable assets.

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

Experimental Challenge

  1. If you haven't done so already, complete the steps within the Initialize Experiment Lab Environment section.
    • This will open the lab environment.
  2. OPTIONAL: If you previously made changes to Vulnerable.sol, you'll need to close Vulnerable.sol and run the revert command within the lab environment's terminal.
  3. Try your best to answer the questions (below).

Questions

  1. 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?
  1. 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 :)

Disclosures

Warning:

  • Content should be used for educational purposes only. You should not leverage this content for nefarious purposes. Blockbash's authors are not liable for misuse of this content.
  • Everyone makes mistakes, including the authors of Blockbash. All content and recommendations should be verified via another source. Blockbash's authors are not liable for any mistakes. If you've found an error, please create a Github Issue.

Appendix

Footnotes

  1. EIP-1884

  2. slither README