Skip links

Table of Contents

Ethernaut Level 21 Walkthrough: Shop

In this article, we will walkthrough Ethernaut Level 21: Shop. This challenge is designed to test your understanding of how view functions interact with state changes in Solidity and how these interactions can be exploited to manipulate contract behavior. 

The goal of this challenge is to purchase an item from the Shop contract for less than the asking price. To achieve this, we need to exploit the way the contract handles pricing and the order of operations within its ‘buy’ function.

ethernaut level 21: shop challenge

Quick Explanation of The Shop Smart Contract

The Shop contract contains two key state variables:

  • ‘price’, a uint256 representing the item’s price, initially set to 100.
  • ‘isSold’, a bool indicating whether the item has been sold, initially set to false.

The contract also includes a ‘buy’ function that is designed to allow users to purchase the item if their offer meets or exceeds the current price and if the item has not already been sold. The logic of the buy function is as follows:

function buy() public {

    Buyer _buyer = Buyer(msg.sender);

    if(_buyer.price() >= price && !isSold) {

        isSold = true;

        price = _buyer.price();

    }

}

In this function, the contract checks the price offered by the buyer by calling the ‘price()’ function on the buyer contract. If the offer is acceptable and the item hasn’t been sold, it updates ‘isSold’ to true and sets the ‘price’ to the buyer’s offered price.

Understanding the Exploit

The key to solving this challenge lies in how view functions interact with state changes. Specifically, the ‘buy’ function expects the buyer’s ‘price()’ function to return a value based on the current state of the Shop contract. By carefully crafting a Buyer contract, we can manipulate this interaction to make the ‘buy’ function accept a lower price.

Initial Approach: Understanding the View Function Limitation

Before arriving at the final solution, it’s important to consider my initial approach and why it wasn’t viable due to Solidity’s restrictions on view functions.

My Initial Idea

The initial strategy was to implement the following logic within the ‘price()’ function:

if(firstcall){

    firstcall = false;

    return 100;

}else{

    return 90;

}

The idea here was to create a ‘price()’ function that behaves differently on consecutive calls. On the first call, it would return 100, and on the second call, it would return 90. This approach would allow the ‘buy()’ function in the Shop contract to first pass the condition with the initial price of 100 and then update the price to 90 on the second internal call within the same transaction. 

However, this approach was fundamentally flawed due to the restrictions imposed by view functions in Solidity. A view function is not allowed to modify the contract’s state. Therefore, in the initial idea, the variable ‘firstcall’ was intended to be used to track whether the ‘price()’ function had been called previously, thus modifying the contract’s state. Since view functions cannot alter state variables, this implementation was wrong.

This first approach made me take this insight and led me to think about the solution. If I could not update the value of a variable in a view function, I would have to “look at” the value of another variable. 

The Final Solution

The breakthrough came by realizing that while the view function itself cannot alter the state, it can read the state of the Shop contract—specifically the ‘isSold’ variable. The ‘isSold’ variable is updated within the ‘buy()’ function before the second call to ‘price()’. This means that the ‘price()’ function can return different values based on whether the item has already been marked as sold.

By leveraging the state of ‘isSold’, we can control the return value of the ‘price()’ function based on the shop’s state without directly altering any state within our Hack contract. This insight led to the development of the final solution, which effectively bypasses the limitations of view functions while still achieving the desired outcome.

To exploit this, we’ll create a contract, ‘Hack’, which implements the Buyer interface and manipulates the ‘price()’ function to return different values based on the ‘isSold’ state of the Shop contract. Here’s the step-by-step guide to implementing the solution using Remix:

Step 1: Set Up in Remix

  1. Open Remix: Go to Remix.
  2. Create a New File: Create a new file named Shop.sol 

Step 2: Define the Hack contract:

Now create a contract which will interact with the ‘Shop’ contract. You can copy the Buyer interface and the Shop contract below it:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Hack {

    Shop private immutable shop;

    constructor(address _target) {

        shop = Shop(_target);

    }

    function buyCheaper() external {

        shop.buy();

        require(shop.price() == 90, "Price is not 90");

    }

    function price() external view returns (uint256) {

        if (!shop.isSold()) {

            return 100;

        } else {

            return 90;

        }

    }

}

Step 3. Compile the contract:

Select the appropriate Solidity compiler version (0.8.0 or above) and compile the contract.

Step 4. Deploy the Hack Contract:

Use the “Deploy & Run Transactions” tab in Remix. Deploy the Hack contract, passing the address of the Shop contract as the constructor argument. 

  1. To get the instance address of Shop contract: Open your browser and navigate to the Ethernaut website. Then, press F12 or Ctrl+Shift+I to open the developer tools to go to the “Console” tab. 
  2. Now click the ‘Get new instance’ button, and right after the transaction gets approved, you will see the instance address of the Shop smart contract in your console.

Step 5: Execute the Exploit

After deploying the contract, execute the ‘buyCheaperfunction()’. This function will:

  • Call the ‘buy()’ function on the Shop contract.
  • The ‘price()’ function in the Hack contract will return 100 during the first call, satisfying the if condition in ‘buy()’ to update ‘isSold’ to true.
  • The ‘price()’ function will then return 90 on the second call within the ‘buy()’ function, reducing the Shop’s price to 90.

Finally, the require statement in the ‘buyCheaper()’ function will verify that the exploit succeeded by checking if the price has been updated to 90.

Conclusion: Ethernaut Level 21

By understanding the interaction between view functions and state changes, we successfully manipulated the Shop contract to sell an item for less than the asking price. This challenge highlights the importance of considering how view functions interact with contract state variables, especially when external contracts are involved.

faq

FAQs

Why can’t the ‘price()’ function modify state within the Hack contract?

  • The ‘price()’ function is declared as a view function, which means it cannot modify the contract’s state. View functions in Solidity are restricted to reading state and not altering it

Why was the initial approach using the ‘firstcall’ variable not feasible?

  • The initial approach involved modifying a state variable (firstcall) within a view function, which is not allowed in Solidity. View functions cannot change the state of the contract, so this approach was invalid.

What role does the ‘isSold’ variable play in this exploit?

  • The ‘isSold’ variable indicates whether the item has been sold. It is updated within the ‘buy()’ function before the second call to the ‘price()’ function, allowing us to return a different price on the second call without modifying any state within the Hack contract.

Why is it important to understand the limitations of view functions in Solidity?

  • Understanding the limitations of view functions is crucial because it affects how you can structure your logic. Misusing view functions can lead to bugs or unintended behavior, especially when dealing with contract interactions and state dependencies.

What is the significance of the ‘require’ statement in the ‘buyCheaper()’ function?

  • The ‘require’ statement in the ‘buyCheaper()’ function ensures that the exploit succeeded by checking that the price of the item in the Shop contract has been.

Could this exploit be prevented?

  • Yes, the exploit could be prevented by avoiding the use of external contracts for critical decisions like pricing or by ensuring that state changes and checks are performed in a single, atomic operation without relying on external contract calls.

Metana Guarantees a Job 💼

Plus Risk Free 2-Week Refund Policy ✨

You’re guaranteed a new job in web3—or you’ll get a full tuition refund. We also offer a hassle-free two-week refund policy. If you’re not satisfied with your purchase for any reason, you can request a refund, no questions asked.

Web3 Solidity Bootcamp

The most advanced Solidity curriculum on the internet!

Full Stack Web3 Beginner Bootcamp

Learn foundational principles while gaining hands-on experience with Ethereum, DeFi, and Solidity.

You may also like

Metana Guarantees a Job 💼

Plus Risk Free 2-Week Refund Policy

You’re guaranteed a new job in web3—or you’ll get a full tuition refund. We also offer a hassle-free two-week refund policy. If you’re not satisfied with your purchase for any reason, you can request a refund, no questions asked.

Web3 Solidity Bootcamp

The most advanced Solidity curriculum on the internet

Full Stack Web3 Beginner Bootcamp

Learn foundational principles while gaining hands-on experience with Ethereum, DeFi, and Solidity.

Learn foundational principles while gaining hands-on experience with Ethereum, DeFi, and Solidity.

Events by Metana

Dive into the exciting world of Web3 with us as we explore cutting-edge technical topics, provide valuable insights into the job market landscape, and offer guidance on securing lucrative positions in Web3.

Subscribe to Lettercamp

We help you land your dream job! Subscribe to find out how

Start Your Application

Secure your spot now. Spots are limited, and we accept qualified applicants on a first come, first served basis..

Career Track(Required)

The application is free and takes just 3 minutes to complete.

What is included in the course?

Expert-curated curriculum

Weekly 1:1 video calls with your mentor

Weekly group mentoring calls

On-demand mentor support

Portfolio reviews by Design hiring managers

Resume & LinkedIn profile reviews

Active online student community

1:1 and group career coaching calls

Access to our employer network

Job Guarantee