Fallback and Receive — Ethernaut Level 1

Kevan Mordan
DraftKings Engineering
4 min readNov 4, 2022

A fallback function is a Solidity-specific function that is executed under two circumstances:

  • Data received, but no function matches the identifiers (including typos!)
  • No data provided with the function call (can send ETH value!)

Because anyone can invoke the fallback function intentionally or accidentally by sending Ether (ETH) to a typo in the function call, special precautions must be placed around any fallback functionality as it can be easily exploited.

Why Fallback?

Fallback was primarily used to react to receiving ETH. Fallback could be used to send ETH to a different address, send tokens, emit events, or any custom functionality desired when receiving ETH. However, because this function can be invoked by anyone, it is very important to understand what it can do in order to prevent security problems.

Fallback is also used for the delegate proxy pattern for upgradable contracts. The contract will fallback and use functions written in other contracts. There will be more on this when we discuss a later level of Ethernaut!

Fallback Functions

Prior to Solidity 0.6.x there was a single fallback function that was declared with a simple anonymous function. The function had no name and was marked as payable to receive ETH. This function handled all cases when fallback was triggered.

Single Fallback Function

Having two conditions that triggered fallback was not only confusing but required supporting all the functionality of those two unique sets of circumstances within a single function. This could easily lead to security problems. Solidity 0.6.x split the single fallback function into two different functions, one for each of those two circumstances that will trigger fallback.

  1. The receive function is executed when the contract is sent Eth with no call data. Must be marked as payable and external and cannot take parameters or return anything.
  2. The fallback function is executed when the contract is called with data that does not match any function identifiers. Must be marked as external and cannot take parameters or return anything.
Split Fallback Functions

The Problem

https://ethernaut.openzeppelin.com/level/0x9CB391dbcD447E645D6Cb55dE6ca23164130D008

Let’s review the contract’s state upon creation by looking at the constructor.

Constructor

Upon instantiation, the contract creator’s address will be added to the contributions map with 1000 ETH. The contributions map just stores the list of addresses with how much each one contributed. Utilizing this base state and the contract’s functions we can figure out how to beat the level:

  1. Claim ownership of the contract
  2. Reduce its balance to 0

Claim Ownership

The first step is to claim ownership of the contract. A good starting point is to find which functions can change the contracts owner.

Ownership Functions

The contribute and fallback receive functions are two that can change the contract’s owner. The contribute function assigns an owner to the sender if the sender has a greater contribution total than the owner. Since the constructor sets the owner’s contributions to the high value of 1000 ETH, a new sender needs to contribute more than 1000 ETH to take ownership. Additionally, because the contribute function requires the value to be <0.001 ETH, this will take a long time.

The receive function requires the value to be greater than 0 ETH and the sender to have made a contribution in the past (>0). This is much easier and faster. All we need to do to take ownership is to make a successful contribution of <0.001 ETH and then transfer any amount of ETH to the contract.

Reduce Balance to Zero

Now that the contract ownership has been claimed all that needs to be done is to drain the balance. Similar to looking for functions that assigned ownership, let’s identify all the functions that can reduce the balance.

Balance Functions

The getContribution function only returns an unsigned integer (uint) of the sender’s total amount of contributions and will not send any ETH or affect the contract’s balance. Additionally, view functions are read-only and cannot transfer funds or modify the state of a contract.

That leaves the withdraw function as the only way to drain the balance. The withdraw function is labeled with the onlyOwner modifier (see Constructor), which requires that the msg.sender be the owner to call the function. Since we have just claimed ownership of the contract, the withdraw function can be called to send the entire contract balance to the owner’s account.

Now that ownership has been claimed and used to empty the contract’s balance the level has been solved!

The next problem, Ethernaut Level 2, will focus on the history of Solidity constructors.

It is simple to copy and paste the contract code into Remix, compile it, and use the level instance address to call functions. In order to call contribute with any value < 0.001 ETH, send any amount of ETH to the contract, then call withdraw. Sending ETH through Remix is done through the Low level interactions field; you could also send ETH via another tool such as Metamask.

--

--