The Ethernaut is a Web3/Solidity based wargame inspired by overthewire.org, played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be ‘hacked’

At the time of writing this post, 27 levels are active in the Ethernaut. This blog is intended to uncover the level1 fallback. To clear this level, the attacker has to exploit a poorly implemented fallback function and gain control of another smart contract.

fallback Function

A fallback function is an external function with neither a name, parameters, nor return values. It is executed in one of the following cases:

  • If a function identifier doesn’t match any of the available functions in a smart contract.
  • If there was no data supplied along with the function call.

Properties of Fallback Function

  • Has no name or arguments.
  • Can not return anything.
  • Can be defined once per contract.
  • It is also executed if the caller meant to call a function that is not available
  • It is mandatory to mark its visibility as external.

Note: The fallback function always receives data, but to also receive Ether it must be marked payable. If the fallback function is not marked payable, the contract will throw an exception if it receives plain ether without data. payable is a mutability modifier in solidity to receive ether.

Challenge

The challenge has a solidity code, that is vulnerable. The code might look intimidating if you are not familiar with Solidity. In short, if executed, the contract can contribute and withdraw.

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

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Fallback {

  using SafeMath for uint256;
  mapping(address => uint) public contributions;
  address payable public owner;

  constructor() public {
    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 {
    owner.transfer(address(this).balance);
  }

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

Solving the challenge

The objective of the challenge is to become the owner of the contract, and this can be achieved in two ways. One is to contribute more than the original owner of the contract and the second is to send to the contract itself.

The below code block handles the first method, since the owner has set the contribution to 1000 ether, completing the challenge is time-consuming.

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

Practically it is possible to send 1000 ETH to the contract on a test network, the catch over here is, it would take up a serious amount of time calling the faucet to get free test ETH into the account.

receive method, on the other hand, has not been declared with the function keyword and it’s payable. This means this method is a fallback function. Hence, the feasible way out of this challenge is to abuse the fallback function. The function changes the owner if two conditions are met,

  1. Sender has contributed to the contract at least once
  2. The second transaction should be above 0
receive() external payable {
  require(msg.value > 0 && contributions[msg.sender] > 0);
  owner = msg.sender;
}

Sending the first contribution

If MetaMask is connected with adequate balance including the gas fees, send a contribution of non-zero value to the contract. If the wallet is connected with the ethernaut platform, executing the below command in the browser console will ensure the first condition is met. The contribute function will only accept a contribution below 0.001 ether (This is mentioned in the code)

contract.contribute({value: toWei("0.00001")})

Becoming the owner

Finally, send a non-zero value to the contract using the sendTransaction function. To confirm the ownership of the contract has been changed to the sender’s address, use the command await contract.owner().

contract.sendTransaction({value: toWei("0.0001")})

Completing the challenge

Besides being the owner of the contract, to complete the challenge, the sender has to withdraw all the amount in the contract using the withdraw() function and submit the instance.

await contract.withdraw()