Fallback | OpenZeppelin Ethernaut Level 1 Walk-through

Level 1 “Fallback” gives us the source code of a contract, to beat the level we must claim ownership of the contract and drain it’s balance to 0.

Level 1

Using the Get new instance button, we deploy our contract.

Console view.

Take a look over the Solidity smart contract code:

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

contract Fallback {

  mapping(address => uint) public contributions;
  address public owner;

  constructor() {
    owner = msg.sender;
    contributions[msg.sender] = 1000 * (1 ether);
  }

  modifier onlyOwner {
        require(
            msg.sender == owner,
            "caller is not the owner"
        );
        _;
    }

  function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }

  function getContribution() public view returns (uint) {
    return contributions[msg.sender];
  }

  function withdraw() public onlyOwner {
    payable(owner).transfer(address(this).balance);
  }

  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }
}

Things that might help

  • How to send ether when interacting with an ABI

  • How to send ether outside of the ABI

  • Converting to and from wei/ether units (see help() command)

  • Fallback methods

From Level 1 Docs


First things first, we need to claim ownership of the contract. Upon reviewing the code, inside of the receive() function it seems we can make ourselves the owner by meeting the requirements.

  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }

According to the Solidity docs, a receive function is used when Ether is sent to the contract without using a defined method.

Contracts that receive Ether directly (without a function call, i.e. using send or transfer) but do not define a receive Ether function or a payable fallback function throw an exception, sending back the Ether (this was different before Solidity v0.4.0). So if you want your contract to receive Ether, you have to implement a receive Ether function (using payable fallback functions for receiving Ether is not recommended, since it would not fail on interface confusions).

Receive Ether Function | Solidity Docs

Looking at the requirement line specifically, “require(msg.value > 0 && contributions[msg.sender] > 0);”
There are two requirements, we need to send some amount of wei with our message, making msg.value > 0.
And we also need to have previous contributions from our address to the contract, therefore contributions[msg.sender] > 0.

  function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }

Let’s call the contribute method.

contract.contribute

Since we know our message value must be under 0.001 ether, I first used the toWei command to convert 0.0009 ether to wei.
Then called the contribute method with that wei value.

Now that we have fulfilled the contribution > 0 requirement, we need to send some ether to that receive function that we looked at earlier.

Sending transaction with empty data.

Here we use the built in ethernaut sendTransaction() method, with the same amount of wei as the value.
We know this will activate the receive function because we send the msg.value with no data.
To ensure we have transferred ownership we can use contract.owner() to verify our address is the owner.

Contract with balance

The contract on Etherscan shows the contract to hold a balance of 0.0018 ether (the total of our 2 transfers; 0.0009 + 0.0009).
The second part of completing the level is reducing the balance to 0.

Looking back at the source code of the contract, there is a withdraw method we can use to withdraw the balance of the contract.

  function withdraw() public onlyOwner {
    payable(owner).transfer(address(this).balance);
  }

So long as we are the owner, this method should withdraw the balance of the contract.

contract.withdraw

After the transaction has been mined we should be able to verify that we have withdrawn our ether back out of the contract.

0 balance contract

All that’s left to do is submit our instance.

Level 1 complete

Congratulations, we finished level 1.

DAVE

GO BACK TO LEVEL 0